diff --git a/client/src/telethon/_impl/client/events/queries.py b/client/src/telethon/_impl/client/events/queries.py index 074cf812..e8aefa92 100644 --- a/client/src/telethon/_impl/client/events/queries.py +++ b/client/src/telethon/_impl/client/events/queries.py @@ -1,9 +1,13 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Dict, Optional, Self +import struct +import typing +from typing import TYPE_CHECKING, Dict, Optional, Self, Union +from .. import errors +from ...session import PackedType from ...tl import abcs, functions, types -from ..types import Chat, Message +from ..types import Chat, Message, Channel, Group from .event import Event from ..types.chat import peer_id from ..client.messages import CherryPickedList @@ -22,7 +26,7 @@ class ButtonCallback(Event): def __init__( self, client: Client, - update: types.UpdateBotCallbackQuery, + update: Union[types.UpdateBotCallbackQuery, types.UpdateInlineBotCallbackQuery], chat_map: Dict[int, Chat], ): self._client = client @@ -33,7 +37,13 @@ class ButtonCallback(Event): def _try_from_update( cls, client: Client, update: abcs.Update, chat_map: Dict[int, Chat] ) -> Optional[Self]: - if isinstance(update, types.UpdateBotCallbackQuery) and update.data is not None: + if ( + isinstance( + update, + (types.UpdateBotCallbackQuery, types.UpdateInlineBotCallbackQuery), + ) + and update.data is not None + ): return cls._create(client, update, chat_map) else: return None @@ -43,6 +53,87 @@ class ButtonCallback(Event): assert self._raw.data is not None return self._raw.data + @property + def via_inline(self) -> bool: + """ + Whether the button was clicked in an inline message. + + If it was, it might indicate that the bot is not in chat. + If this is the case, both the :meth:`chat` and :meth:`get_message` will return :data:`None`. + """ + return isinstance(self._raw, types.UpdateInlineBotCallbackQuery) + + @property + def message_id(self) -> typing.Union[int, abcs.InputBotInlineMessageId]: + """ + The identifier of the message containing the button that was clicked. + + If the message is inline, :class:`abcs.InputBotInlineMessageId` will be returned. + You can use it in :meth:`~telethon._tl.functions.messages.edit_inline_bot_message` to edit the message. + + Else, usual message ID will be returned. + """ + return self._raw.msg_id + + @property + def chat(self) -> Optional[Chat]: + """ + The :term:`chat` where the button was clicked. + + This may be :data:`None` if :data:`via_inline` is :data:`True`, as the bot might not be part of the chat. + """ + if isinstance(self._raw, types.UpdateInlineBotCallbackQuery): + # for that type of update, the msg_id and owner_id are present, however bot is not guaranteed + # to have "access" to the owner_id. + if isinstance(self._raw.msg_id, types.InputBotInlineMessageId): + # telegram used to pack msg_id and peer_id into InputBotInlineMessageId.id + # I assume this is for the chats with IDs, fitting into 32-bit integer. + _, owner_id = struct.unpack( + " 0: + # We can't know if it's really a chat with user, or an ID of the user who issued the inline query. + # So it's better to return None, than to return wrong chat. + return None + + owner_id = -owner_id + + if owner := self._chat_map.get(owner_id): + return owner + + packed = self.client._chat_hashes.get(owner_id) + + raw = types.ChannelForbidden( + broadcast=False, + megagroup=False, + id=owner_id, + access_hash=0, + title="", + until_date=None, + ) + + if packed: + raw.access_hash = packed.access_hash + + if packed.ty == PackedType.MEGAGROUP or packed.ty == PackedType.GIGAGROUP: + if packed.ty == PackedType.GIGAGROUP: + raw.gigagroup = True + else: + raw.megagroup = True + return Group._from_raw(self.client, raw) + raw.broadcast = True + return Channel._from_raw(raw) + return self._chat_map.get(peer_id(self._raw.peer)) + async def answer( self, text: Optional[str] = None, @@ -75,20 +166,42 @@ class ButtonCallback(Event): """ Get the :class:`~telethon.types.Message` containing the button that was clicked. - If the message is too old and is no longer accessible, :data:`None` is returned instead. + If the message is inline, or too old and is no longer accessible, :data:`None` is returned instead. """ + chat = self.chat - pid = peer_id(self._raw.peer) - chat = self._chat_map.get(pid) - if not chat: - chat = await self._client._resolve_to_packed(pid) + # numeric_id, received in this case, is: + # - a correct message id, if it was sent in a channel (or megagroup, gigagroup) + # - a sender's message id, if it was sent in a private chat. So it's not a correct ID from bot perspective, + # as each account has its own message id counter for private chats (pm, small group chats). + if isinstance(self._raw, types.UpdateInlineBotCallbackQuery): + if isinstance(self._raw.msg_id, types.InputBotInlineMessageId): + numeric_id, _ = struct.unpack(" Chat: """ - The chat where messages are sent in this dialog. + The :term:`chat` where messages are sent in this dialog. """ return self._chat_map[peer_id(self._raw.peer)] diff --git a/client/src/telethon/_impl/client/types/draft.py b/client/src/telethon/_impl/client/types/draft.py index 4568c613..0d41297f 100644 --- a/client/src/telethon/_impl/client/types/draft.py +++ b/client/src/telethon/_impl/client/types/draft.py @@ -56,7 +56,7 @@ class Draft(metaclass=NoPublicConstructor): @property def chat(self) -> Chat: """ - The chat where the draft is saved. + The :term:`chat` where the draft is saved. This is also the chat where the message will be sent to by :meth:`send`. """