Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

6 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Telegram Forwarder logo

Telegram Forwarder

A self-healing Telegram userbot that mirrors messages between chats β€” copies, edits, read-states and all β€” even from copy-protected channels. Controlled from your own account or purely via config file.

Python Pyrogram Checked with mypy Linted with ruff Platform


Table of Contents

Features

  • Chat-to-chat forwarding β€” copies messages (text and every media type) from source chats into any number of target chats.
  • Works with copy-protected chats β€” channels and groups with "Restrict saving content" enabled (typical for premium/paid channels) block normal forwarding and copying; the re-upload fallback mirrors them anyway, for every media type.
  • Two-stage copy fallback β€” tries Telegram's native copy first; when that is blocked, it transparently downloads and re-uploads the media as a fresh message.
  • Edit propagation β€” edits to a source message are mirrored into all targets; optionally the full edit history is rendered into the message (store_edits).
  • Direction filters & exclusions β€” forward only incoming, only outgoing, or both (copy_incoming / copy_outgoing); skip messages matching fnmatch patterns.
  • View-once mode β€” a per-chat setting (view_once) for self-destructing media (view-once photos & videos, listen-once voice/video notes): off skips them, on forwards them alongside everything else, only forwards nothing but them. In on/only they are archived as regular, persistent media in the target chat before they can burn.
  • Sender attribution β€” optionally prefix copied text with the original sender's name (--name-prefix).
  • Read-state mirroring β€” when you read a source chat, its targets are marked read too; optionally targets are flagged unread on fresh copies (mark_target_unread).
  • Daily keep-alives β€” per-chat daily messages with a random time offset, message pools, and {timestamp} / {date} / {time} placeholders.
  • Hot-reload β€” bot_config.json is reloaded on change without a restart; a corrupt edit never crashes the bot.
  • Self-healing β€” active health-check with hard-exit for supervisor restarts, plus a two-layer reconnect strategy.
  • Scheduled commands β€” slash commands work from Telegram's scheduled-messages view, with replies that never leak to the chat partner.
  • Optional control bot β€” a separate BotFather bot with an inline-button menu to review the config, toggle per-chat settings (direction, view-once, keep-alive, …) and add new rules by picking from your recent chats β€” all locked to your own user id.
  • Fully headless if you want β€” one env switch disables all chat commands; the bot then runs purely off the JSON config.

How It Works

The bot logs in as a regular user account (userbot, not a bot account) and listens to its own message stream:

flowchart LR
    S["πŸ“₯ Source chat"] -->|"new / edited message"| L["Listener"]
    L --> R{"Rules<br/>exclusions Β· direction"}
    R -->|skip| X(["ignored"])
    R -->|forward| P["Copy pipeline"]
    P -->|"1️⃣ message.copy()"| T1["🎯 Target chat A"]
    P -->|"2️⃣ fallback:<br/>download + re-upload"| T2["🎯 Target chat B"]
    P <-->|"edit propagation"| FI[("Forward index<br/>(source β†’ targets, TTL)")]
Loading

Every copied message is tracked in an in-memory forward index, so later edits of the source can be applied to the exact target messages β€” in place when possible, delete-and-resend when not.

Because the fallback re-uploads content as fresh messages, even chats with Telegram's content protection ("Restrict saving content", common in premium channels) can be mirrored β€” where native forwarding and copying are refused by the API.

Installation

Requires Python 3.12+. Pick whichever style you prefer β€” all of them end up with the same bot.

Option 1 β€” Clone & run (no install)

git clone https://github.com/bjspi/telegram-forwarder.git
cd telegram-forwarder
pip install -r requirements.txt
python bot.py

Update later:

git pull
pip install -r requirements.txt   # in case dependencies changed

Option 2 β€” pipx (recommended for a CLI)

pipx installs the bot into its own isolated environment and puts a telegram-forwarder command on your PATH:

pipx install git+https://github.com/bjspi/telegram-forwarder.git
telegram-forwarder

Update later:

pipx upgrade telegram-forwarder

Option 3 β€” pip

Straight from GitHub, no checkout needed:

pip install git+https://github.com/bjspi/telegram-forwarder.git
telegram-forwarder

Update later: pip install -U --force-reinstall git+https://github.com/bjspi/telegram-forwarder.git

Or as an editable install from a checkout (best for hacking on the code β€” git pull is the whole update):

git clone https://github.com/bjspi/telegram-forwarder.git
cd telegram-forwarder
pip install -e .
telegram-forwarder      # or still: python bot.py

First start

The bot reads its credentials from a .env file in the working directory:

cp .env.example .env    # Windows: copy .env.example .env
# then fill in TG_ACCOUNT, TG_PHONE_NR, TG_API_ID, TG_API_HASH
# (get API id + hash at https://my.telegram.org/auth?to=apps)

The first start walks you through the Pyrogram login (phone number + code) and creates session/, logs/ and bot_config.json. Those contain secrets or private data and are all gitignored.

Tip

tgcrypto (included in the dependencies) speeds up MTProto encryption massively β€” make sure it is installed in the active environment.

Configuration

Environment (.env)

Everything an operator tunes lives in .env β€” every variable is documented in .env.example. The important ones:

Variable Default Purpose
TG_ACCOUNT, TG_PHONE_NR, TG_API_ID, TG_API_HASH β€” Required Telegram credentials; startup aborts with a clear error if missing.
ENABLE_CHAT_COMMANDS true Master switch for all slash commands (see headless mode).
COMMANDS_CHAT_ID 0 Restrict commands to a single chat id; 0 = every chat (see command scope).
CONTROL_BOT_TOKEN / CONTROL_BOT_OWNER_ID β€” Optional BotFather control bot and its sole owner (see Control Bot).
TIMEZONE Europe/Berlin Keep-alive scheduling and log timestamps.
DELETE_DELAY_SECONDS 5 Auto-delete delay for command replies.
FORWARD_RECORD_TTL_SECONDS 86400 How long source→target mappings are kept for edit propagation.
HEALTH_CHECK_INTERVAL_SECONDS / MAX_UNHEALTHY_DURATION_SECONDS 60 / 900 Health-check cadence and the hard-exit threshold.
LOG_RETENTION_DAYS 10 Daily log files older than this are deleted automatically.

Runtime config (bot_config.json)

Listener and keep-alive rules live in bot_config.json. It is normally managed through chat commands, but you can also edit it by hand β€” the file is hot-reloaded on change, missing fields are migrated transparently, and a corrupt edit simply keeps the previous in-memory state.

{
  "listeners": [
    {
      "source_chat_id": -1001234567890,
      "target_chats": [{ "chat_id": -1009876543210, "chat_name": "My Archive" }],
      "exclusions": ["*spoiler*"],   // fnmatch patterns on text/caption
      "name_prefix": false,
      "copy_incoming": true,
      "copy_outgoing": true,
      "store_edits": false,
      "mark_target_unread": false,
      "view_once": "off"             // "off" skip Β· "on" include Β· "only" exclusive
    }
  ],
  "keepalives": [
    {
      "chat_id": 123456789,
      "time_of_day": "08:00:00",
      "time_of_day_delta": 1.5,      // random Β± offset in hours, per day
      "message_text": ["Good morning {date}!", "Ping ({time})"]
    }
  ]
}

Headless mode (no chat commands)

Maybe you don't want the bot reacting to anything typed in chats β€” you'd rather configure everything centrally in the JSON file. Set:

ENABLE_CHAT_COMMANDS=false

With commands disabled, no command handlers are registered at all: the bot is purely passive, only forwards according to bot_config.json, and slash messages you type stay untouched. Forwarding, keep-alives, hot-reload and the health-check are unaffected.

Chat Commands

With ENABLE_CHAT_COMMANDS=true (the default), the bot is controlled from your own account. Commands are gated by filters.me β€” only your own slash messages trigger anything, and every command plus its reply deletes itself after a short delay.

Command Effect
/helpf Show the help text.
/show_id Show the current chat id.
/list_chats List the 20 most recent chats with type, name and id.
/config Β· /config <source> Β· /config all Show listener configuration.
/copy <target> [more...] [--name-prefix] [--view-once=off|on|only] Use the current chat as source and add targets.
/copy <source> <target> [more...] Create a rule from any chat.
/stop Β· /stop <source> Β· /stop <source> <target> Remove listeners or single connections.
/keepalive [HH:MM:SS] Β· /keepalive pause Β· /keepalive off Manage the chat's daily keep-alive.

Scheduled commands: commands sent from Telegram's scheduled-messages view are answered back into the scheduled queue β€” the reply stays visible for 30 seconds, starts with (AutoDelete in 30s), and is then removed together with the command, so it never reaches the chat partner.

Restricting commands to one chat

By default the command handlers listen in every chat (as long as you send the slash command). If you'd rather keep them out of your normal conversations, point COMMANDS_CHAT_ID at a single chat β€” typically a private group containing only yourself β€” and the handlers are registered scoped to that chat only:

COMMANDS_CHAT_ID=-1001234567890

Now /copy, /stop, /keepalive etc. only do anything in that one group; the same slash command typed anywhere else is left completely untouched. Forwarding, keep-alives and everything else keep working across all configured chats regardless. To find the id, run /show_id in the target group. Set it back to 0 to allow commands everywhere again.

How to make a group with just yourself: create a new group, add any contact, then remove them again β€” Telegram keeps the group with you as the only member. It's the tidiest command console for a userbot.

Control Bot

Optionally, a separate BotFather bot gives you an inline-button menu to inspect the configuration and flip per-chat settings β€” no need to type slash commands into a chat. It runs alongside the userbot in the same process and edits the same live config, so changes take effect immediately.

Set it up with two variables:

CONTROL_BOT_TOKEN=123456:ABC-DEF...      # from @BotFather
CONTROL_BOT_OWNER_ID=123456789           # your own numeric user id

Then send /menu to your bot. Every screen is locked to CONTROL_BOT_OWNER_ID β€” the bot ignores everyone else, and it refuses to start at all if no owner is set. The menu lets you:

  • πŸ“Š Overview β€” the full listener + keep-alive configuration at a glance.
  • πŸ“‘ Listeners β€” pick any configured source chat and toggle, with live βœ… state on every button:
    • Direction β€” incoming Β· outgoing Β· both
    • View-once media β€” off Β· on Β· only
    • Name prefix, Store edits, Mark target unread
    • Keep-alive β€” enable/disable the chat's daily ping (if one exists)
    • βž• Add listener β€” build a new rule (see below)
  • ⏰ Keep-alives β€” enable or disable each keep-alive with one tap, or βž• Add keep-alive.

Creating rules from the menu: Add listener and Add keep-alive walk you through a short flow. For each chat you either pick from your 20 most recently active chats (the userbot fetches them live β€” no need to remember ids) or type a chat id / @username, which is verified by resolving it before anything is saved. Keep-alive times are entered as HH:MM:SS and validated. Everything else β€” targets, message pools, exclusions β€” is still configured with the userbot's /copy / /keepalive commands or by editing bot_config.json.

The control bot also registers its own command menu (/menu, /start) via the Bot API, so the commands show up in Telegram's UI.

Architecture

bot.py is a thin entry point; the application is the telegram_forwarder package right next to it (flat layout, same pattern as yt-dlp or HTTPie) β€” so both clone & run and pip/pipx install work naturally.

flowchart TD
    B["bot.py β€” entry point"] --> A["app.py β€” composition root + run loop"]
    A --> H["handlers/<br/>commands Β· listeners Β· filters"]
    A --> C["connection/<br/>client Β· chats Β· health"]
    A --> CB["control_bot/<br/>client Β· keyboards Β· views Β· handlers"]
    H --> S["services/<br/>forwarding Β· keepalive Β· enrichment"]
    H --> U["ui/<br/>formatting Β· replies"]
    CB --> K
    S --> K["core/<br/>models Β· config_store Β· forward_index Β· rules Β· naming"]
    C --> K
    U --> K
    K --> E["settings.py β€” .env + constants"]
Loading
bot.py                        # entry point: imports the package and runs the app
telegram_forwarder/
β”œβ”€β”€ settings.py               # .env loading, credentials, all tuning constants
β”œβ”€β”€ logging_setup.py          # daily log files with retention, logging config
β”œβ”€β”€ app.py                    # composition root: client + scheduler + handlers + run loop
β”œβ”€β”€ core/                     # domain: models, config store, forward index, message rules
β”œβ”€β”€ connection/               # Pyrogram client factory, chat resolution, health check
β”œβ”€β”€ services/                 # forwarding pipeline, keep-alive scheduling, name enrichment
β”œβ”€β”€ handlers/                 # Pyrogram handlers: slash commands + message listeners
β”œβ”€β”€ control_bot/              # optional BotFather bot: inline-menu config UI
└── ui/                       # reply formatting and auto-delete replies

Reliability

Built to run unattended under a process supervisor (supervisord, systemd, NSSM, …):

  1. Outer reconnect loop β€” network/RPC errors during startup trigger a retry after NETWORK_RETRY_DELAY_SECONDS.
  2. Active health check β€” Pyrogram retries connection failures internally without ever raising, which can leave a bot silently dead. A periodic get_me() ping detects this; if the connection stays down longer than MAX_UNHEALTHY_DURATION_SECONDS (default 15 min), the process hard-exits (os._exit(1)) so the supervisor restarts it.
  3. Crash forensics β€” unexpected fatal errors are written to logs/critical_error_*.txt; daily logs rotate at midnight and expire after LOG_RETENTION_DAYS.

Development

pip install ruff mypy
ruff check bot.py telegram_forwarder    # lint / bug finder
mypy bot.py telegram_forwarder          # static type check

Both tools are configured in pyproject.toml. Project conventions: type hints and docstrings on every function, @dataclass(slots=True) for state containers, and everything operator-tunable lives in .env.


Note

This is a userbot: it logs in with your own Telegram account via the MTProto API. Use it responsibly and in accordance with Telegram's Terms of Service β€” aggressive automation can get accounts limited.

About

Self-healing Telegram userbot that mirrors messages between chats - copies, edits & read-states, even from copy-protected channels. Pyrogram + APScheduler.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages