fixes that were dropping messages: - d->tg sent parse_mode=Markdown with an unescaped body, so any message with a stray * _ ` or [ got a 400 from telegram and vanished. now html with proper escaping + a plain-text retry - no allowed_mentions on webhook posts, so telegram could @everyone - forwarder.start() ran from on_ready, which fires again on every gateway reconnect -> second poller on the same token -> 409s and dupes - document.mime_type is nullable and was called directly - sticker finally block hit UnboundLocalError when mkstemp raised - ffmpeg timeouts leaked temp files and orphaned the process stickers, per format instead of one blanket gif convert: - static .webp now passes through (the gif convert crushed 228 alpha levels to 2 and made the file 4x bigger) - video .webm -> animated webp, keeps 8-bit alpha, ~7x smaller than gif. needs -c:v libvpx-vp9 before -i or ffmpeg silently drops the alpha - .tgs is gzipped lottie, ffmpeg exits 183 on it and we were uploading the raw file. renders via optional lottie lib, else thumbnail + emoji added: telegram profile photos as webhook avatars (re-hosted in a private channel, AVATAR_CACHE_CHANNEL_ID), reply threading both ways, edit sync both ways, delete sync d->tg, album batching, voice/poll/location/contact/ dice/forward attribution, utf-16-safe entity conversion, mention and custom emoji resolution, message splitting instead of truncation, shared aiohttp session with 429 backoff, route discovery from env, !bridge status main.py just loads the extension now; routes come from credentials.env Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01RXrxDFpyq3VjvxWGuW3CZe
90 lines
2.9 KiB
Python
90 lines
2.9 KiB
Python
#two-way telegram <-> discord bridge, loaded as an extension##
|
|
"""tgbridge — two-way Telegram <-> Discord bridge.
|
|
|
|
Polling starts at extension load, not on_ready: on_ready fires again after
|
|
every gateway reconnect and a second poller on one token means telegram 409s.
|
|
"""
|
|
######################################################################
|
|
import asyncio
|
|
import logging
|
|
import os
|
|
|
|
from discord.ext import commands
|
|
|
|
from . import config
|
|
from .discord_to_telegram import DiscordToTelegramForwarder
|
|
from .forwarder import TelegramForwarder
|
|
from .status_cog import BridgeStatus
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
_forwarder: TelegramForwarder | None = None
|
|
|
|
|
|
async def setup(bot: commands.Bot) -> None:
|
|
global _forwarder
|
|
|
|
token = os.getenv("TELEGRAM_BOT_TOKEN")
|
|
if not token:
|
|
log.warning("TELEGRAM_BOT_TOKEN is not set — bridge not loaded")
|
|
return
|
|
|
|
routes = config.load_routes()
|
|
if not routes:
|
|
log.warning("no bridge routes configured — bridge not loaded")
|
|
return
|
|
|
|
replay = os.getenv("TELEGRAM_REPLAY_BACKLOG", "").lower() in ("1", "true", "yes")
|
|
|
|
_forwarder = TelegramForwarder(
|
|
token=token,
|
|
discord_bot=bot,
|
|
avatar_channel_id=config.avatar_channel_id(),
|
|
routes=routes,
|
|
drop_pending=not replay,
|
|
)
|
|
reverse = DiscordToTelegramForwarder(bot=bot, tg_token=token, routes=routes)
|
|
|
|
await bot.add_cog(reverse)
|
|
await bot.add_cog(BridgeStatus(bot, _forwarder, reverse))
|
|
bot.tg_bridge = _forwarder
|
|
|
|
# setup() runs before the gateway connects, so raising here stops the whole
|
|
# bot booting. retry in the background instead; !bridge status shows it.
|
|
try:
|
|
await _forwarder.start()
|
|
except Exception as exc:
|
|
log.error("telegram bridge failed to start (%s) — retrying in the "
|
|
"background; the rest of the bot is unaffected", exc)
|
|
_forwarder.last_error = f"start failed: {exc}"
|
|
# keep the reference; a bare task can be gc'd mid-flight
|
|
bot.tg_bridge_retry = asyncio.create_task(_retry_start(_forwarder))
|
|
|
|
|
|
async def _retry_start(forwarder) -> None:
|
|
delay = 30
|
|
for attempt in range(5):
|
|
await asyncio.sleep(delay)
|
|
try:
|
|
await forwarder.start()
|
|
log.info("telegram bridge recovered on attempt %d", attempt + 1)
|
|
return
|
|
except Exception as exc:
|
|
log.warning("telegram bridge retry %d failed: %s", attempt + 1, exc)
|
|
forwarder.last_error = f"start failed: {exc}"
|
|
delay = min(delay * 2, 600)
|
|
log.error("telegram bridge gave up after 5 retries — restart the bot")
|
|
|
|
|
|
async def teardown(bot: commands.Bot) -> None:
|
|
global _forwarder
|
|
task = getattr(bot, "tg_bridge_retry", None)
|
|
if task and not task.done():
|
|
task.cancel()
|
|
if _forwarder is not None:
|
|
await _forwarder.stop()
|
|
_forwarder = None
|
|
|
|
|
|
__all__ = ["TelegramForwarder", "DiscordToTelegramForwarder", "setup", "teardown"]
|