From 2b26984698db3b7d6d564121bff3a785292125de Mon Sep 17 00:00:00 2001 From: esefxdz Date: Mon, 22 Jun 2026 21:59:29 +0300 Subject: [PATCH] telegram forwarding method added --- main.py | 75 +++++--- tgbridge/__init__.py | 1 + tgbridge/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 163 bytes .../__pycache__/forwarder.cpython-312.pyc | Bin 0 -> 11491 bytes tgbridge/forwarder.py | 179 ++++++++++++++++++ 5 files changed, 225 insertions(+), 30 deletions(-) create mode 100644 tgbridge/__init__.py create mode 100644 tgbridge/__pycache__/__init__.cpython-312.pyc create mode 100644 tgbridge/__pycache__/forwarder.cpython-312.pyc create mode 100644 tgbridge/forwarder.py diff --git a/main.py b/main.py index 38dc6b6..6593e7f 100644 --- a/main.py +++ b/main.py @@ -1,30 +1,45 @@ -import discord -from discord.ext import commands -from dotenv import load_dotenv -import os -import logging -import asyncio - -load_dotenv('credentials.env') - -intents = discord.Intents.default() -intents.message_content = True - -bot = commands.Bot(command_prefix='!', intents=intents) - -@bot.event -async def on_ready(): - print(f'✅ {bot.user} is online!') - -async def main(): - async with bot: - await bot.load_extension('cogs.general') - await bot.load_extension('cogs.chance') - await bot.load_extension('cogs.gifs') - await bot.load_extension('cogs.copypasta') - await bot.load_extension('cogs.sysinfo') - await bot.load_extension('music.music') - await bot.start(os.getenv('DISCORD_TOKEN')) - -logging.basicConfig(level=logging.INFO) -asyncio.run(main()) \ No newline at end of file +import discord +from discord.ext import commands +from dotenv import load_dotenv +import os +import logging +import asyncio + +from tgbridge.forwarder import TelegramForwarder + +load_dotenv('credentials.env') + +intents = discord.Intents.default() +intents.message_content = True + +bot = commands.Bot(command_prefix='!', intents=intents) + +@bot.event +async def on_ready(): + print(f'✅ {bot.user} is online!') + +async def main(): + # ── telegram forwarder ─────────────────────────────────────────────── + forwarder = TelegramForwarder( + token=os.getenv('TELEGRAM_BOT_TOKEN'), + group_id=int(os.getenv('TELEGRAM_GROUP_ID')), + webhook_url=os.getenv('DISCORD_WEBHOOK_URL'), + ) + + async with bot: + await bot.load_extension('cogs.general') + await bot.load_extension('cogs.chance') + await bot.load_extension('cogs.gifs') + await bot.load_extension('cogs.copypasta') + await bot.load_extension('cogs.sysinfo') + await bot.load_extension('music.music') + + await forwarder.start() + + try: + await bot.start(os.getenv('DISCORD_TOKEN')) + finally: + await forwarder.stop() + +logging.basicConfig(level=logging.INFO) +asyncio.run(main()) \ No newline at end of file diff --git a/tgbridge/__init__.py b/tgbridge/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tgbridge/__init__.py @@ -0,0 +1 @@ + diff --git a/tgbridge/__pycache__/__init__.cpython-312.pyc b/tgbridge/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f52be0280f8d73562ce0193e7341e7b7b5039188 GIT binary patch literal 163 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 bQ(3IMC2m^(ivAP*84bOU{+J>!LixV|ry22j literal 0 HcmV?d00001 diff --git a/tgbridge/forwarder.py b/tgbridge/forwarder.py new file mode 100644 index 0000000..ab718e0 --- /dev/null +++ b/tgbridge/forwarder.py @@ -0,0 +1,179 @@ +# this is where telegram-to-discord forwarding lives +import logging +import io +import json +import aiohttp +from telegram import Update +from telegram.ext import Application, MessageHandler, filters, ContextTypes + +logger = logging.getLogger('telegram.forwarder') + + +class TelegramForwarder: + """Watches a Telegram group/channel and forwards messages to a Discord webhook.""" + + def __init__(self, token: str, group_id: int, webhook_url: str): + self.token = token + self.group_id = group_id + self.webhook_url = webhook_url + self._app: Application | None = None + + async def start(self): + """Build the telegram application and start polling.""" + self._app = ( + Application.builder() + .token(self.token) + .build() + ) + self._app.add_handler(MessageHandler(filters.ALL, self._on_message)) + + await self._app.initialize() + await self._app.start() + await self._app.updater.start_polling(drop_pending_updates=True) + logger.info('✅ Telegram forwarder started — watching group %s', self.group_id) + + async def stop(self): + """Graceful shutdown.""" + if self._app: + await self._app.updater.stop() + await self._app.stop() + await self._app.shutdown() + logger.info('Telegram forwarder stopped') + + # ── internal helpers ───────────────────────────────────────────────────── + + def _sender_name(self, update: Update) -> str: + """Extract a display name from the message sender.""" + user = update.effective_user + if user: + name = user.first_name or '' + if user.last_name: + name += f' {user.last_name}' + return name.strip() or user.username or 'Unknown' + # channel posts have no user — use chat title + chat = update.effective_chat + if chat and chat.title: + return chat.title + return 'Unknown' + + def _forward_prefix(self, message) -> str: + """Build a prefix for forwarded messages.""" + if message.forward_origin: + origin = message.forward_origin + # user forward + if hasattr(origin, 'sender_user') and origin.sender_user: + u = origin.sender_user + name = u.first_name or '' + if u.last_name: + name += f' {u.last_name}' + return f'↩️ Forwarded from **{name.strip() or u.username or "Unknown"}**:\n' + # channel forward + if hasattr(origin, 'chat') and origin.chat: + return f'↩️ Forwarded from **{origin.chat.title or "Unknown channel"}**:\n' + # hidden user + if hasattr(origin, 'sender_user_name') and origin.sender_user_name: + return f'↩️ Forwarded from **{origin.sender_user_name}**:\n' + return '↩️ Forwarded:\n' + return '' + + async def _download_file(self, file_id: str) -> tuple[bytes, str]: + """Download a file from Telegram, return (bytes, filename).""" + tg_file = await self._app.bot.get_file(file_id) + buf = io.BytesIO() + await tg_file.download_to_memory(buf) + buf.seek(0) + filename = tg_file.file_path.split('/')[-1] if tg_file.file_path else 'file' + return buf.read(), filename + + async def _send_to_webhook(self, username: str, content: str = '', + file_data: bytes | None = None, + filename: str | None = None): + """POST a message (and optional file) to the Discord webhook.""" + async with aiohttp.ClientSession() as session: + data = aiohttp.FormData() + payload = {'username': username} + if content: + payload['content'] = content[:2000] # discord limit + + if file_data and filename: + data.add_field('payload_json', json.dumps(payload), + content_type='application/json') + data.add_field('file', file_data, filename=filename) + else: + # simple json post + async with session.post(self.webhook_url, json=payload) as resp: + if resp.status not in (200, 204): + logger.error('Webhook POST failed: %s %s', resp.status, await resp.text()) + return + + async with session.post(self.webhook_url, data=data) as resp: + if resp.status not in (200, 204): + logger.error('Webhook POST failed: %s %s', resp.status, await resp.text()) + + # ── main message handler ───────────────────────────────────────────────── + + async def _on_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """Handle every incoming message from the watched group.""" + message = update.effective_message + if not message: + return + + # only process messages from the configured group + if update.effective_chat and update.effective_chat.id != self.group_id: + return + + sender = self._sender_name(update) + prefix = self._forward_prefix(message) + caption = message.caption or '' + + try: + # ── photo ──────────────────────────────────────────────────── + if message.photo: + # largest resolution is last in the array + photo = message.photo[-1] + file_data, filename = await self._download_file(photo.file_id) + text = f'{prefix}{caption}'.strip() + await self._send_to_webhook(sender, text, file_data, filename) + + # ── video ──────────────────────────────────────────────────── + elif message.video: + file_data, filename = await self._download_file(message.video.file_id) + text = f'{prefix}{caption}'.strip() + await self._send_to_webhook(sender, text, file_data, filename) + + # ── document (files, gifs sent as docs, etc) ───────────────── + elif message.document: + file_data, filename = await self._download_file(message.document.file_id) + text = f'{prefix}{caption}'.strip() + await self._send_to_webhook(sender, text, file_data, filename) + + # ── animation (GIF) ────────────────────────────────────────── + elif message.animation: + file_data, filename = await self._download_file(message.animation.file_id) + text = f'{prefix}{caption}'.strip() + await self._send_to_webhook(sender, text, file_data, filename) + + # ── sticker ────────────────────────────────────────────────── + elif message.sticker: + # stickers have no caption — send emoji or name + sticker_text = message.sticker.emoji or '(sticker)' + text = f'{prefix}{sticker_text}' + # try to download the sticker image + try: + file_data, filename = await self._download_file(message.sticker.file_id) + await self._send_to_webhook(sender, text, file_data, filename) + except Exception: + await self._send_to_webhook(sender, text) + + # ── text ───────────────────────────────────────────────────── + elif message.text: + text = f'{prefix}{message.text}' + await self._send_to_webhook(sender, text) + + else: + # voice, audio, contact, location, etc — just note it + text = f'{prefix}*(unsupported message type)*' + await self._send_to_webhook(sender, text) + + except Exception: + logger.exception('Failed to forward message from %s', sender)