| """ |
| This is where the rubber meets the road: real Telethon events get converted |
| into the compat Update/Context objects the rest of the codebase (unchanged) |
| expects, mirroring how python-telegram-bot's Application dispatched updates. |
| |
| main.py constructs one Application and calls .run(...) on it - see that file |
| for the top-level wiring. |
| """ |
|
|
| import asyncio |
| import logging |
| import signal |
| import traceback |
|
|
| from telethon import TelegramClient, events |
|
|
| from ._bot import Bot |
| from ._message import Message, CallbackQuery, InlineQuery, Update |
| from ._types import TelegramUser |
| from ._jobqueue import JobQueue |
| from .error import BadRequest |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| class Context: |
| """Mirrors telegram.ext.CallbackContext / ContextTypes.DEFAULT_TYPE.""" |
|
|
| def __init__(self, bot, bot_data, user_data, application, job=None): |
| self.bot = bot |
| self.bot_data = bot_data |
| self.user_data = user_data |
| self.application = application |
| self.job = job |
|
|
| @property |
| def job_queue(self): |
| return self.application.job_queue |
|
|
|
|
| class ContextTypes: |
| """Mirrors telegram.ext.ContextTypes.""" |
|
|
| DEFAULT_TYPE = Context |
|
|
|
|
| |
| CallbackContext = Context |
|
|
|
|
| def _is_chatid_command(text: str) -> bool: |
| if not text: |
| return False |
| first_word = text.split()[0] if text.split() else "" |
| command = first_word.split("@")[0] |
| return command.lower() == "/chatid" |
|
|
|
|
| class Application: |
| """Mirrors telegram.ext.Application. Owns the single TelegramClient for |
| the process, plus the in-memory bot_data/user_data stores PTB would |
| otherwise manage (this project uses no persistence backend, so plain |
| dicts that live for the process lifetime are an exact match).""" |
|
|
| def __init__( |
| self, |
| api_id: int, |
| api_hash: str, |
| bot_token: str, |
| session: str = "bot_session", |
| timezone=None, |
| connect_timeout: int = 20, |
| connection_retries: int = 5, |
| ): |
| self._bot_token = bot_token |
| self._client = TelegramClient( |
| session, |
| api_id, |
| api_hash, |
| timeout=connect_timeout, |
| connection_retries=connection_retries, |
| |
| |
| |
| |
| |
| flood_sleep_threshold=0, |
| ) |
| self.bot_data = {} |
| self._user_data_store = {} |
| self.bot = Bot(self._client) |
| self.job_queue = JobQueue(self, timezone=timezone) |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| self._background_tasks = set() |
|
|
| self._regular_handler = None |
| self._callback_handler = None |
| self._chatid_handler = None |
|
|
| def new_context(self, update: Update = None, job=None) -> Context: |
| user_id = None |
| if update is not None and update.effective_user is not None: |
| user_id = update.effective_user.id |
| user_data = self._user_data_store.setdefault(user_id, {}) if user_id is not None else {} |
| return Context(bot=self.bot, bot_data=self.bot_data, user_data=user_data, application=self, job=job) |
|
|
| def create_task(self, coro, name: str = None): |
| async def _wrapped(): |
| try: |
| await coro |
| except BadRequest as e: |
| message = str(e).lower() |
| if "cannot start conversations" in message or "invalid peer" in message: |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| logger.info(f"Background task {name or '<unnamed>'}: recipient unreachable, skipping ({e})") |
| else: |
| logger.exception(f"Unhandled exception in background task {name or '<unnamed>'}") |
| await self.bot.report_error( |
| f"Unhandled exception in background task {name or '<unnamed>'}:\n\n{traceback.format_exc()}" |
| ) |
| except Exception: |
| logger.exception(f"Unhandled exception in background task {name or '<unnamed>'}") |
| await self.bot.report_error( |
| f"Unhandled exception in background task {name or '<unnamed>'}:\n\n{traceback.format_exc()}" |
| ) |
|
|
| task = asyncio.ensure_future(_wrapped()) |
| self._background_tasks.add(task) |
| task.add_done_callback(self._background_tasks.discard) |
| return task |
|
|
| |
|
|
| def set_handlers(self, regular_handler, callback_handler, chatid_handler): |
| self._regular_handler = regular_handler |
| self._callback_handler = callback_handler |
| self._chatid_handler = chatid_handler |
|
|
| def _register_event_handlers(self): |
| client = self._client |
|
|
| @client.on(events.NewMessage()) |
| async def _on_new_message(event): |
| if event.out: |
| |
| |
| |
| return |
| try: |
| message = await Message.from_telethon(event.message, client, self.bot) |
| except Exception: |
| logger.exception("tg_compat: failed to build Message from NewMessage event") |
| await self.bot.report_error(f"tg_compat: failed to build Message from NewMessage event:\n\n{traceback.format_exc()}") |
| return |
|
|
| update = Update(message=message, _bot=self.bot) |
| context = self.new_context(update=update) |
|
|
| if _is_chatid_command(message.text): |
| await self._chatid_handler(update, context) |
| return |
|
|
| await self._regular_handler(update, context) |
|
|
| @client.on(events.ChatAction()) |
| async def _on_chat_action(event): |
| if event.action_message is None: |
| return |
| try: |
| message = await Message.from_chat_action(event, client, self.bot) |
| except Exception: |
| logger.exception("tg_compat: failed to build Message from ChatAction event") |
| await self.bot.report_error(f"tg_compat: failed to build Message from ChatAction event:\n\n{traceback.format_exc()}") |
| return |
| update = Update(message=message, _bot=self.bot) |
| context = self.new_context(update=update) |
| await self._regular_handler(update, context) |
|
|
| @client.on(events.CallbackQuery()) |
| async def _on_callback_query(event): |
| try: |
| data = event.data.decode("utf-8") if isinstance(event.data, (bytes, bytearray)) else event.data |
| except Exception: |
| data = None |
|
|
| tl_message = None |
| try: |
| tl_message = await event.get_message() |
| except Exception: |
| logger.warning("tg_compat: could not fetch message for callback query", exc_info=True) |
|
|
| message = None |
| if tl_message is not None: |
| try: |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| message = await Message.from_telethon(tl_message, client, self.bot) |
| except Exception: |
| logger.exception("tg_compat: failed to build Message for callback query") |
| await self.bot.report_error(f"tg_compat: failed to build Message for callback query:\n\n{traceback.format_exc()}") |
|
|
| sender = await event.get_sender() |
| from_user = TelegramUser.from_entity(sender, client) |
|
|
| callback_query = CallbackQuery( |
| id=str(event.query.query_id), |
| data=data, |
| message=message, |
| from_user=from_user, |
| _client=client, |
| ) |
| update = Update(callback_query=callback_query, _bot=self.bot) |
| context = self.new_context(update=update) |
| await self._callback_handler(update, context) |
|
|
| @client.on(events.InlineQuery()) |
| async def _on_inline_query(event): |
| sender = await event.get_sender() |
| from_user = TelegramUser.from_entity(sender, client) |
| inline_query = InlineQuery(id=str(event.id), query=event.text, from_user=from_user, _event=event) |
| update = Update(inline_query=inline_query, _bot=self.bot) |
| context = self.new_context(update=update) |
| await self._regular_handler(update, context) |
|
|
| async def run(self, post_init=None, drop_pending_updates: bool = True): |
| await self._client.start(bot_token=self._bot_token) |
|
|
| me = await self._client.get_me() |
| self.bot.id = me.id |
| self.bot.username = me.username |
|
|
| self._register_event_handlers() |
|
|
| if post_init is not None: |
| await post_init(self) |
|
|
| if not drop_pending_updates: |
| await self._client.catch_up() |
|
|
| logger.info(f"Bot connected as @{me.username} (id={me.id})") |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| loop = asyncio.get_running_loop() |
|
|
| def _request_shutdown(sig_name: str): |
| logger.info(f"Received {sig_name}, shutting down...") |
| self.create_task(self._client.disconnect(), name="shutdown-disconnect") |
|
|
| for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGABRT): |
| try: |
| loop.add_signal_handler(sig, _request_shutdown, sig.name) |
| except NotImplementedError: |
| |
| |
| |
| |
| |
| pass |
|
|
| await self._client.run_until_disconnected() |
| logger.info("Bot stopped.") |
|
|