Back in 2020, I wrote an article on vc.ru “Creating and deploying a Telegram channel relay using Python and Heroku”. Since then, Heroku no longer offers a free plan. But the project is still alive and being updated.

</> Project repository: https://github.com/khoben/telemirror

Actually, there is nothing supernatural about forwarding: we receive a message from the source channel and send a copy of it to the receiving channel.

In the simplest case, it looks like this:

from telethon import TelegramClient, events

client = TelegramClient("telemirror", api_id, api_hash)
SOURCE_CHANNELS = [-10001, -10002] # channel IDs
TARGET_CHANNELS = [-10003, -10004]

@client.on(events.NewMessage(chats=SOURCE_CHANNELS))
async def handler(event):
    for target in TARGET_CHANNELS:
        await client.send_message(target, event.message)

client.start()
client.run_until_disconnected()

Receiving a message → Sending a message

The only thing missing is some configuration and flexibility. Which messages to discard, whether to change the original text, from which channel to forward to which channel, etc.

Modifiers, which can be called filters, are added to the simple scheme described above.

In TeleMirror, the simplest filter for messages looks like this:

class MessageFilter(Protocol):
    async def process(
        self, entity: EventEntity, event_type: Type[EventLike]
    ) -> FilterResult[EventEntity]:
        if isinstance(entity, EventMessage):
            # Message processing: continue, modify, discard
            return await self._process_message(entity, event_type)

        if isinstance(entity, list):
            # Processing a group of messages - an album
            return await self._process_album(entity, event_type)

        return FilterResult(FilterAction.CONTINUE, entity)

Filters can modify messages or discard them.

Such filters can be combined into a group that will execute sequentially, processing the message:

class CompositeMessageFilter(MessageFilter):
    def __init__(self, filters: List[MessageFilter]) -> None:
        self._filters = filters

    async def process(
        self, entity: EventEntity, event_type: Type[EventLike]
    ) -> FilterResult[EventEntity]:
        for f in self._filters:
            filter_action, entity = await f.process(entity, event_type)
            match filter_action:
                case FilterAction.CONTINUE | True:
                    continue
                case FilterAction.DISCARD | False:
                    return FilterResult(FilterAction.DISCARD, entity)
                case FilterAction.FORCE_SEND:
                    return FilterResult(FilterAction.FORCE_SEND, entity)

        return FilterResult(FilterAction.CONTINUE, entity)

Message reception → (filters) → Sending (or not) the processed message

The configuration for forwarding looks like this:

CHAT_MAPPING: Dict[int, Dict[int, List["DirectionConfig"]]] = {}

@dataclass
class DirectionConfig:
    disable_delete: bool
    disable_edit: bool
    filters: MessageFilter
    from_topic_id: Optional[int] = None
    to_topic_id: Optional[int] = None
    mode: Literal["copy", "forward"] = "copy"

In general, the configuration for retransmission consists of channel mapping (from->to) and the configuration for this direction.

Example of YAML configuration

# Forwarding directions
directions:
  - from: [-1001, -1002, -1003] # Source channels
    to: [-100203] # Receiver channels

  - from: [-1000#3] # Forwarding from topic to topic
    to: [-1001#4]

  - from: [-100226]
    to: [-1006, -1008]
    disable_edit: false
    disable_delete: false
    mode: forward  # Forwarding type: copy or forward
    filters: # List of filters that will be applied sequentially
      - UrlMessageFilter:
          blacklist: !!set
            ? t.me
      - KeywordReplaceFilter:
          keywords:
            "google.com": "bing.com"
            "r'google\\.com.*'": "bing.com"
      - SkipWithKeywordsFilter:
          keywords: !!set
            ? "stopword"
            ? "r'badword.*'"

</> For more details, see the project repository: https://github.com/khoben/telemirror

Possible problems

Account ban or session deactivation

The retransmitter does not behave like a normal user: it is always online, does not view ads, etc. Do not use your main account as a userbot for the relay. Newly created accounts are at high risk of being banned, so it is worth using them as a regular user for a while.

New messages are not coming through

Telegram optimizes message delivery, so not all updates may come through in real time. Sometimes, updates start coming in after the next restart.

Where to run

Unfortunately, at the time of writing, I don’t know of any service that offers a free plan for continuous operation of the application without crashes. You’ll have to search for one or rent your own virtual server, which can be useful not only for the relay.