A chat bridge that mirrors messages and medias between platforms (Currently it's Discord, Slack and Telegram) and is built so you can bolt on other platforms such as WhatsApp, Messenger and more later by writing a single connector class.
Every platform is essentially a peer, and each one runs when its credentials are configured, and a single bridge can span all of them at once.
Remote users show up with their own name and avatar (IF within platforms limitation) not as one generic bot account.
- Per-user identity. Remote users are posted under their own name + avatar where the platform supports it (Discord webhooks, Slack message customization).
- Pairing-code setup. No hardcoded channel IDs, run
/setupon one side, type the code into/setup <code>on the other. - Multiple bridges. Link as many channel ↔ chat pairs as you like.
- All media types. Photos, videos, GIFs/animations, audio, voice notes, video notes, stickers, and documents at both directions. (The old bot silently dropped Telegram photos, videos, and stickers.)
- Replies, edits, and deletes follow the message across platforms (deletes propagate from Discord and Slack; Telegram's API never reports them, so a Telegram deletion can't be mirrored).
- Reply-pings. Replying to a bridged message pings the original author on the other platform.
- Cross-platform @mentions (optional). @-mention a Discord member from
Telegram (and vice versa).
/linkconnects accounts with different names,/whofinds someone's handle. - Persistent SQLite storage with automatic pruning of old message links.
- Safe by default.
@everyone/@herefrom a remote platform can't ping, and bot tokens are never leaked into message text.
-
Install dependencies (Python 3.10+):
pip install -r requirements.txt
-
Create the bots and get their tokens. You need at least two platforms (each starts only if its credentials are set). Step-by-step instructions for every token, scope, and setting are in CREDENTIALS.md. In short:
- Discord - bot token + Message Content Intent; invite with Manage Webhooks, Send Messages, Read Message History.
- Telegram - bot token from @BotFather; disable Group Privacy and re-add the bot to the group.
- Slack - enable Socket Mode (app token), add bot scopes + install (bot
token), subscribe to
message.channels, create the slash commands, and/invitethe bot to the channel.
-
Configure:
cp .env.example .env # then edit .env and paste in the tokens for the platforms you're using -
Run:
python main.py
- In the Discord channel, run
/setup. The bot replies (only to you) with a pairing code likeK7QMP4. - In the Telegram group, send
/setup K7QMP4within 15 minutes. - Done, messages now flow both ways. Either side can start the process; the other side just needs the code.
Other commands: /status (show what a channel is linked to) and /unlink
(disconnect it). In Telegram groups these are admin-only; on Discord they need
Manage Server. On Slack, because it reserves some names, /status is
/bridgestatus and /who is /whois, everything else is the same.
Reply-pings - always on. Reply to someone's bridged message and the original
author gets pinged: a real @ mention on Discord, a @user mention on
Telegram. No configuration needed.
Cross-platform @mentions - optional. Set ENABLE_CROSS_MENTIONS=true to let
people @mention users on the other platform. When someone types @handle,
it's rewritten into a real ping on the destination platform. Resolution has two
layers:
-
Native members - no setup.
@usernameis matched against the real member directory of the destination platform:- From Telegram → any Discord member, by username / display name / nickname.
- From Discord → any Telegram user who has posted in the group.
An exact username match wins over a fuzzy one, so
@onion_ringsreachesonion_ringseven ifonion_rings.also exists. Anything ambiguous stays plain text, nobody is mis-pinged. -
Linked accounts -
/link. Discord and Telegram are separate identity systems, so there's no safe way to guess that "Alice" on one is "Alice" on the other./linklets a person bond their two accounts into one identity, so either of their handles pings them on either side, useful when their names differ across platforms.- Run
/linkon one platform → get a code. - Run
/link <code>on the other within 15 minutes → connected.
/unlinkmedisconnects. (Telegram users need a Telegram @username to be mentionable by handle.) - Run
Finding a handle - /who <name>. Only see someone's display name and don't
know their username? /who onion searches the other side of the bridge and
returns the exact @handle to type.
Notes & limits:
- A mention is a single token - multi-word display names (e.g. "Do I love Onion Rings?") can't be a mention target; use the username (
@onion_rings). This is true on Discord and Telegram natively too. - The rewrite happens on the destination platform, not the one you type on.
Typing
@onion_ringson Telegram stays plain there but lands as a ping on Discord. @everyone/@hereand role mentions from a remote platform are always blocked.
The core (teleport/router.py) only knows the Connector interface in
teleport/connectors/base.py. To add e.g. WhatsApp:
- Subclass
Connector, set a uniqueplatformstring, and set the capability flags (supports_impersonation,supports_native_reply,reports_deletions). - On each inbound message, build a
BridgeMessageand callawait self.router.on_message(...)(pluson_edit/on_deleteif supported). - Implement
send_message/edit_message/delete_message. - Register it in
main.py.
No routing, storage, or other-connector code needs to change, and a bridge can span more than two platforms at once.
/setup, /link, and @mentions come almost for free. The pairing and
identity-linking flows live on the Router, not in any connector. To support
them on a new platform you only write thin adapters that (a) pull the platform's
native user id / username and (b) reply, the actual logic is shared:
# a new platform's /link handler, in full:
if not self.router.enable_cross_mentions:
return reply("Cross-platform mentions are off.")
if code:
other = await self.router.complete_identity_link(code, self.platform, uid, username, display)
reply("Linked!" if other else "Invalid or expired code.")
else:
code = await self.router.begin_identity_link(self.platform, uid, username, display)
reply(f"Your link code: {code}")
# and to rewrite @mentions in outgoing text:
await self.router.resolve_mention(handle, self.platform) # -> linked account or NoneOptionally implement search_directory(channel_id, query) (returns
[(display_name, handle)]) and your platform is searchable from /who; skip it
and it just returns nothing.
Finally, add the platform's env vars to config.py + .env.example, start the
connector in main.py when they're present, and document how to obtain its
tokens in CREDENTIALS.md (there's a copy-paste template at
the bottom of that file).
main.py entrypoint / wiring
teleport/
config.py env loading
models.py BridgeMessage, Attachment, ReplyContext, Endpoint
db.py async SQLite storage
router.py fans messages out to linked endpoints
util.py shared helpers (codes, chunking, sizes)
connectors/
base.py the Connector interface
discord_connector.py
telegram_connector.py
slack_connector.py