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.
- Features
- How It Works
- Installation
- Configuration
- Chat Commands
- Control Bot
- Architecture
- Reliability
- Development
- 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
copyfirst; 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 matchingfnmatchpatterns. - View-once mode β a per-chat setting (
view_once) for self-destructing media (view-once photos & videos, listen-once voice/video notes):offskips them,onforwards them alongside everything else,onlyforwards nothing but them. Inon/onlythey 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.jsonis 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.
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)")]
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.
Requires Python 3.12+. Pick whichever style you prefer β all of them end up with the same bot.
git clone https://github.com/bjspi/telegram-forwarder.git
cd telegram-forwarder
pip install -r requirements.txt
python bot.pyUpdate later:
git pull
pip install -r requirements.txt # in case dependencies changedpipx 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-forwarderUpdate later:
pipx upgrade telegram-forwarderStraight from GitHub, no checkout needed:
pip install git+https://github.com/bjspi/telegram-forwarder.git
telegram-forwarderUpdate 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.pyThe 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.
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. |
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.
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=falseWith 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.
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.
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=-1001234567890Now /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.
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 idThen 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.
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"]
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
Built to run unattended under a process supervisor (supervisord, systemd, NSSM, β¦):
- Outer reconnect loop β network/RPC errors during startup trigger a retry after
NETWORK_RETRY_DELAY_SECONDS. - 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 thanMAX_UNHEALTHY_DURATION_SECONDS(default 15 min), the process hard-exits (os._exit(1)) so the supervisor restarts it. - Crash forensics β unexpected fatal errors are written to
logs/critical_error_*.txt; daily logs rotate at midnight and expire afterLOG_RETENTION_DAYS.
pip install ruff mypy
ruff check bot.py telegram_forwarder # lint / bug finder
mypy bot.py telegram_forwarder # static type checkBoth 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.
{ "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})"] } ] }