tgbridge: rework both directions — avatars, stickers, replies, edits
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
This commit is contained in:
1 parent
f6ba74394f
commit
eac6dbda3a
12 files changed
+2325
-307
No files matched your search
@@ -0,0 +1,99 @@
|
||||
"""!bridge — health and routing readout for the Telegram bridge."""
|
||||
######################################################################
|
||||
import logging
|
||||
import time
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from . import media, store
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _uptime(started_at) -> str:
|
||||
if not started_at:
|
||||
return "not running"
|
||||
seconds = int(time.time() - started_at)
|
||||
days, seconds = divmod(seconds, 86400)
|
||||
hours, seconds = divmod(seconds, 3600)
|
||||
minutes, seconds = divmod(seconds, 60)
|
||||
if days:
|
||||
return f"{days}d {hours}h {minutes}m"
|
||||
if hours:
|
||||
return f"{hours}h {minutes}m"
|
||||
return f"{minutes}m {seconds}s"
|
||||
|
||||
|
||||
class BridgeStatus(commands.Cog):
|
||||
def __init__(self, bot, forwarder, reverse):
|
||||
self.bot = bot
|
||||
self.forwarder = forwarder
|
||||
self.reverse = reverse
|
||||
|
||||
@commands.command(name="bridge")
|
||||
async def bridge(self, ctx, action: str = "status"):
|
||||
"""Show Telegram bridge routes, counters and health."""
|
||||
if action != "status":
|
||||
await ctx.reply("usage: `!bridge status`")
|
||||
return
|
||||
|
||||
forwarder = self.forwarder
|
||||
embed = discord.Embed(
|
||||
title="Telegram bridge",
|
||||
colour=discord.Colour.green() if forwarder and forwarder._running
|
||||
else discord.Colour.red())
|
||||
|
||||
embed.add_field(
|
||||
name="Polling",
|
||||
value=("running — up " + _uptime(forwarder.started_at))
|
||||
if forwarder and forwarder._running else "stopped",
|
||||
inline=True)
|
||||
|
||||
avatars = forwarder.avatars if forwarder else None
|
||||
embed.add_field(
|
||||
name="Avatars",
|
||||
value=(f"on ({avatars.hits} hits, {avatars.uploads} uploads)"
|
||||
if avatars and avatars.enabled
|
||||
else "off — set AVATAR_CACHE_CHANNEL_ID"),
|
||||
inline=True)
|
||||
|
||||
embed.add_field(
|
||||
name="Lottie (.tgs)",
|
||||
value="installed" if media.lottie_available() else "not installed",
|
||||
inline=True)
|
||||
|
||||
lines = []
|
||||
for chat_id, route in (forwarder.routes.items() if forwarder else []):
|
||||
channel = self.bot.get_channel(route.dc_channel_id) \
|
||||
if route.dc_channel_id else None
|
||||
target = channel.mention if channel else f"`{route.dc_channel_id}`"
|
||||
arrows = "↔" if route.dc_to_tg and route.tg_to_dc else "→"
|
||||
lines.append(f"**{route.name}** `{chat_id}` {arrows} {target}")
|
||||
embed.add_field(name=f"Routes ({len(lines)})",
|
||||
value="\n".join(lines) or "none configured",
|
||||
inline=False)
|
||||
|
||||
webhook = forwarder.webhook if forwarder else None
|
||||
counters = [
|
||||
f"TG → Discord: **{forwarder.forwarded if forwarder else 0}**",
|
||||
f"Discord → TG: **{self.reverse.forwarded if self.reverse else 0}**",
|
||||
]
|
||||
if webhook:
|
||||
counters.append(f"webhook posts: {webhook.sent} ok / {webhook.failed} failed")
|
||||
try:
|
||||
counters.append(f"cached avatars: {await store.count_avatars()}")
|
||||
counters.append(f"cached stickers: {await store.count_media()}")
|
||||
except Exception:
|
||||
pass
|
||||
embed.add_field(name="Counters", value="\n".join(counters), inline=False)
|
||||
|
||||
errors = [e for e in (getattr(forwarder, "last_error", None),
|
||||
getattr(self.reverse, "last_error", None),
|
||||
getattr(webhook, "last_error", None)) if e]
|
||||
if errors:
|
||||
embed.add_field(name="Last error",
|
||||
value="```" + "\n".join(errors)[:900] + "```",
|
||||
inline=False)
|
||||
|
||||
await ctx.reply(embed=embed)
|
||||
Reference in new issue
Block a user