From 8948c8cef9609de00dcfbba2b6d5b6f4d5356978 Mon Sep 17 00:00:00 2001 From: apepenkov Date: Sat, 11 Nov 2023 22:59:16 +0300 Subject: [PATCH 01/13] update ButtonCallback to handle types.UpdateInlineBotCallbackQuery --- .../telethon/_impl/client/events/queries.py | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/client/src/telethon/_impl/client/events/queries.py b/client/src/telethon/_impl/client/events/queries.py index 074cf8122..fbd786841 100644 --- a/client/src/telethon/_impl/client/events/queries.py +++ b/client/src/telethon/_impl/client/events/queries.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Dict, Optional, Self +from typing import TYPE_CHECKING, Dict, Optional, Self, Union from ...tl import abcs, functions, types from ..types import Chat, Message @@ -22,7 +22,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 +33,10 @@ def __init__( 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 +46,16 @@ def data(self) -> bytes: assert self._raw.data is not None return self._raw.data + @property + def chat(self) -> Optional[Chat]: + """ + The :term:`chat` when the message was sent. + Only available if the event was triggered by a button under usual message, not an inline one. + """ + if isinstance(self._raw, types.UpdateInlineBotCallbackQuery): + return None + return self._chat_map.get(peer_id(self._raw.peer)) + async def answer( self, text: Optional[str] = None, @@ -75,8 +88,10 @@ async def get_message(self) -> Optional[Message]: """ 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. """ + if isinstance(self._raw, types.UpdateInlineBotCallbackQuery): + return None pid = peer_id(self._raw.peer) chat = self._chat_map.get(pid) From c2509ade099814c18430573dc512acee1dab3b9a Mon Sep 17 00:00:00 2001 From: apepenkov Date: Sat, 11 Nov 2023 23:00:01 +0300 Subject: [PATCH 02/13] fix docstring typo in ButtonCallback.chat --- client/src/telethon/_impl/client/events/queries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/telethon/_impl/client/events/queries.py b/client/src/telethon/_impl/client/events/queries.py index fbd786841..97447a327 100644 --- a/client/src/telethon/_impl/client/events/queries.py +++ b/client/src/telethon/_impl/client/events/queries.py @@ -50,7 +50,7 @@ def data(self) -> bytes: def chat(self) -> Optional[Chat]: """ The :term:`chat` when the message was sent. - Only available if the event was triggered by a button under usual message, not an inline one. + Only available if the event was triggered by a button under a usual message, not an inline one. """ if isinstance(self._raw, types.UpdateInlineBotCallbackQuery): return None From 51c2db90c43e78db9c4924535d597f14a8ce7ec2 Mon Sep 17 00:00:00 2001 From: apepenkov Date: Sat, 11 Nov 2023 23:00:49 +0300 Subject: [PATCH 03/13] replace chat with :term:chat in some docstrings: --- client/src/telethon/_impl/client/types/dialog.py | 2 +- client/src/telethon/_impl/client/types/draft.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/telethon/_impl/client/types/dialog.py b/client/src/telethon/_impl/client/types/dialog.py index 54357a11f..56cf63d21 100644 --- a/client/src/telethon/_impl/client/types/dialog.py +++ b/client/src/telethon/_impl/client/types/dialog.py @@ -49,7 +49,7 @@ def _from_raw( @property def chat(self) -> 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 4568c613c..0d41297f1 100644 --- a/client/src/telethon/_impl/client/types/draft.py +++ b/client/src/telethon/_impl/client/types/draft.py @@ -56,7 +56,7 @@ def _from_raw( @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`. """ From 69e741f2cfbccb85a0ca3e073b3fa1ef90fc0dca Mon Sep 17 00:00:00 2001 From: Lonami Date: Sun, 12 Nov 2023 15:20:38 +0100 Subject: [PATCH 04/13] Update client/src/telethon/_impl/client/events/queries.py --- client/src/telethon/_impl/client/events/queries.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/src/telethon/_impl/client/events/queries.py b/client/src/telethon/_impl/client/events/queries.py index 97447a327..c68a1d52c 100644 --- a/client/src/telethon/_impl/client/events/queries.py +++ b/client/src/telethon/_impl/client/events/queries.py @@ -49,8 +49,9 @@ def data(self) -> bytes: @property def chat(self) -> Optional[Chat]: """ - The :term:`chat` when the message was sent. - Only available if the event was triggered by a button under a usual message, not an inline one. + The :term:`chat` where the button was clicked. + + This will be :data:`None` if the message with the button was sent from a user's inline query. """ if isinstance(self._raw, types.UpdateInlineBotCallbackQuery): return None From 508a0ab59e8ddf34f2307c16c4607f3da82de6c3 Mon Sep 17 00:00:00 2001 From: apepenkov Date: Sun, 17 Dec 2023 22:35:48 +0300 Subject: [PATCH 05/13] Implemented get_message, chat for types.UpdateInlineBotCallbackQuery for ButtonCallback event --- .../telethon/_impl/client/events/queries.py | 98 ++++++++++++++++--- 1 file changed, 85 insertions(+), 13 deletions(-) diff --git a/client/src/telethon/_impl/client/events/queries.py b/client/src/telethon/_impl/client/events/queries.py index c68a1d52c..00c392847 100644 --- a/client/src/telethon/_impl/client/events/queries.py +++ b/client/src/telethon/_impl/client/events/queries.py @@ -1,9 +1,10 @@ from __future__ import annotations +import struct from typing import TYPE_CHECKING, Dict, Optional, Self, Union from ...tl import abcs, functions, types -from ..types import Chat, Message +from ..types import Chat, Message, Channel from .event import Event from ..types.chat import peer_id from ..client.messages import CherryPickedList @@ -34,7 +35,10 @@ def _try_from_update( cls, client: Client, update: abcs.Update, chat_map: Dict[int, Chat] ) -> Optional[Self]: if ( - isinstance(update, (types.UpdateBotCallbackQuery, types.UpdateInlineBotCallbackQuery)) + isinstance( + update, + (types.UpdateBotCallbackQuery, types.UpdateInlineBotCallbackQuery), + ) and update.data is not None ): return cls._create(client, update, chat_map) @@ -51,10 +55,41 @@ def chat(self) -> Optional[Chat]: """ The :term:`chat` where the button was clicked. - This will be :data:`None` if the message with the button was sent from a user's inline query. + This will be :data:`None` if the message with the button was sent from a user's inline query, except in channel. """ if isinstance(self._raw, types.UpdateInlineBotCallbackQuery): - return None + owner_id = None + if isinstance(self._raw.msg_id, types.InputBotInlineMessageId): + _, 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, then to return wrong chat. + return None + + owner_id = -owner_id + + access_hash = 0 + packed = self.client._chat_hashes.get(owner_id) + if packed: + access_hash = packed.access_hash + + return Channel._from_raw( + types.ChannelForbidden( + broadcast=True, + megagroup=False, + id=owner_id, + access_hash=access_hash, + title="", + until_date=None, + ) + ) return self._chat_map.get(peer_id(self._raw.peer)) async def answer( @@ -91,20 +126,57 @@ async def get_message(self) -> Optional[Message]: If the message is inline, or too old and is no longer accessible, :data:`None` is returned instead. """ + chat = None + if isinstance(self._raw, types.UpdateInlineBotCallbackQuery): - return None + # 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. + msg_id, owner_id = struct.unpack( + " Date: Sun, 17 Dec 2023 22:39:53 +0300 Subject: [PATCH 06/13] fix Channel.pack()'s PackedType check --- client/src/telethon/_impl/client/types/chat/channel.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/client/src/telethon/_impl/client/types/chat/channel.py b/client/src/telethon/_impl/client/types/chat/channel.py index b498f4c53..63e5a7843 100644 --- a/client/src/telethon/_impl/client/types/chat/channel.py +++ b/client/src/telethon/_impl/client/types/chat/channel.py @@ -54,10 +54,13 @@ def pack(self) -> Optional[PackedChat]: if self._raw.access_hash is None: return None else: + ty = PackedType.BROADCAST + if getattr(self._raw, "megagroup", False): + ty = PackedType.MEGAGROUP + elif getattr(self._raw, "gigagroup", False): + ty = PackedType.GIGAGROUP return PackedChat( - ty=PackedType.GIGAGROUP - if getattr(self._raw, "gigagroup", False) - else PackedType.BROADCAST, + ty=ty, id=self._raw.id, access_hash=self._raw.access_hash, ) From 7408a43495f38bcf096318a3bfabf8173b965851 Mon Sep 17 00:00:00 2001 From: apepenkov Date: Sun, 17 Dec 2023 22:42:24 +0300 Subject: [PATCH 07/13] fix Group.pack()'s PackedType check --- client/src/telethon/_impl/client/types/chat/group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/telethon/_impl/client/types/chat/group.py b/client/src/telethon/_impl/client/types/chat/group.py index 031e18af3..7232ade03 100644 --- a/client/src/telethon/_impl/client/types/chat/group.py +++ b/client/src/telethon/_impl/client/types/chat/group.py @@ -72,7 +72,7 @@ def pack(self) -> Optional[PackedChat]: return None else: return PackedChat( - ty=PackedType.MEGAGROUP, + ty=PackedType.GIGAGROUP if getattr(self._raw, "gigagroup", False) else PackedType.MEGAGROUP, id=self._raw.id, access_hash=self._raw.access_hash, ) From 8fdc255468fda819bb978cea2298de943edd51b6 Mon Sep 17 00:00:00 2001 From: apepenkov Date: Sun, 17 Dec 2023 23:21:02 +0300 Subject: [PATCH 08/13] add via_inline and message_id to ButtonCallback --- .../telethon/_impl/client/events/queries.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/client/src/telethon/_impl/client/events/queries.py b/client/src/telethon/_impl/client/events/queries.py index 00c392847..9b4e0b341 100644 --- a/client/src/telethon/_impl/client/events/queries.py +++ b/client/src/telethon/_impl/client/events/queries.py @@ -1,6 +1,7 @@ from __future__ import annotations import struct +import typing from typing import TYPE_CHECKING, Dict, Optional, Self, Union from ...tl import abcs, functions, types @@ -50,6 +51,29 @@ def data(self) -> bytes: 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, most likely bot is not in chat, and the :meth:`chat` property will return :data:`None`, + same for :meth:`get_message` method, however editing the message, using :meth:`message_id` property + and :meth:`answer` method will work. + """ + return isinstance(self._raw, types.UpdateInlineBotCallbackQuery) + + @property + def message_id(self) -> typing.Union[int, abcs.InputBotInlineMessageId]: + """ + The ID 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]: """ From fe87031fbd650798fcdf11915d703c76acc617e0 Mon Sep 17 00:00:00 2001 From: apepenkov <39992738+apepenkov@users.noreply.github.com> Date: Sun, 17 Dec 2023 23:56:36 +0300 Subject: [PATCH 09/13] Update client/src/telethon/_impl/client/events/queries.py Co-authored-by: Lonami --- client/src/telethon/_impl/client/events/queries.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/src/telethon/_impl/client/events/queries.py b/client/src/telethon/_impl/client/events/queries.py index 9b4e0b341..dc46d1b37 100644 --- a/client/src/telethon/_impl/client/events/queries.py +++ b/client/src/telethon/_impl/client/events/queries.py @@ -56,9 +56,8 @@ def via_inline(self) -> bool: """ Whether the button was clicked in an inline message. - If it was, most likely bot is not in chat, and the :meth:`chat` property will return :data:`None`, - same for :meth:`get_message` method, however editing the message, using :meth:`message_id` property - and :meth:`answer` method will work. + 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) From c0e09e9820f18071fcb3ffb5e52b9a4c7939ab2d Mon Sep 17 00:00:00 2001 From: apepenkov <39992738+apepenkov@users.noreply.github.com> Date: Sun, 17 Dec 2023 23:56:57 +0300 Subject: [PATCH 10/13] Update client/src/telethon/_impl/client/types/chat/channel.py Co-authored-by: Lonami --- client/src/telethon/_impl/client/types/chat/channel.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/telethon/_impl/client/types/chat/channel.py b/client/src/telethon/_impl/client/types/chat/channel.py index 63e5a7843..e57d1d764 100644 --- a/client/src/telethon/_impl/client/types/chat/channel.py +++ b/client/src/telethon/_impl/client/types/chat/channel.py @@ -54,11 +54,12 @@ def pack(self) -> Optional[PackedChat]: if self._raw.access_hash is None: return None else: - ty = PackedType.BROADCAST if getattr(self._raw, "megagroup", False): ty = PackedType.MEGAGROUP elif getattr(self._raw, "gigagroup", False): ty = PackedType.GIGAGROUP + else: + ty = PackedType.BROADCAST return PackedChat( ty=ty, id=self._raw.id, From 5b33b4b9c197e741fb6f0f7eebfa1de52bf642be Mon Sep 17 00:00:00 2001 From: apepenkov <39992738+apepenkov@users.noreply.github.com> Date: Sun, 17 Dec 2023 23:57:03 +0300 Subject: [PATCH 11/13] Update client/src/telethon/_impl/client/events/queries.py Co-authored-by: Lonami --- client/src/telethon/_impl/client/events/queries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/telethon/_impl/client/events/queries.py b/client/src/telethon/_impl/client/events/queries.py index dc46d1b37..340b1c1d5 100644 --- a/client/src/telethon/_impl/client/events/queries.py +++ b/client/src/telethon/_impl/client/events/queries.py @@ -78,7 +78,7 @@ def chat(self) -> Optional[Chat]: """ The :term:`chat` where the button was clicked. - This will be :data:`None` if the message with the button was sent from a user's inline query, except in channel. + 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): owner_id = None From fbf0adb87b72ab8c5aeb95c5fa0644e64a2449f6 Mon Sep 17 00:00:00 2001 From: apepenkov Date: Mon, 18 Dec 2023 17:30:01 +0300 Subject: [PATCH 12/13] update get_message and @property chat for ButtonCallback --- .../telethon/_impl/client/events/queries.py | 115 +++++++++--------- 1 file changed, 59 insertions(+), 56 deletions(-) diff --git a/client/src/telethon/_impl/client/events/queries.py b/client/src/telethon/_impl/client/events/queries.py index 340b1c1d5..d0b2f3183 100644 --- a/client/src/telethon/_impl/client/events/queries.py +++ b/client/src/telethon/_impl/client/events/queries.py @@ -4,8 +4,10 @@ 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, Channel +from ..types import Chat, Message, Channel, Group from .event import Event from ..types.chat import peer_id from ..client.messages import CherryPickedList @@ -64,8 +66,8 @@ def via_inline(self) -> bool: @property def message_id(self) -> typing.Union[int, abcs.InputBotInlineMessageId]: """ - The ID of the message containing the button that was clicked. - + 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. @@ -81,38 +83,56 @@ def chat(self) -> Optional[Chat]: 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. owner_id = None if isinstance(self._raw.msg_id, types.InputBotInlineMessageId): - _, 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, then to return wrong chat. + # So it's better to return None, than to return wrong chat. return None owner_id = -owner_id - access_hash = 0 + if owner := self._chat_map.get(owner_id): + return owner + packed = self.client._chat_hashes.get(owner_id) - if packed: - access_hash = packed.access_hash - - return Channel._from_raw( - types.ChannelForbidden( - broadcast=True, - megagroup=False, - id=owner_id, - access_hash=access_hash, - title="", - until_date=None, - ) + + 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( @@ -149,54 +169,37 @@ async def get_message(self) -> Optional[Message]: If the message is inline, or too old and is no longer accessible, :data:`None` is returned instead. """ - chat = None + chat = self.chat + # 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): - # 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. - msg_id, owner_id = struct.unpack( - " Date: Mon, 18 Dec 2023 17:34:29 +0300 Subject: [PATCH 13/13] fix typo --- client/src/telethon/_impl/client/events/queries.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/telethon/_impl/client/events/queries.py b/client/src/telethon/_impl/client/events/queries.py index d0b2f3183..e8aefa927 100644 --- a/client/src/telethon/_impl/client/events/queries.py +++ b/client/src/telethon/_impl/client/events/queries.py @@ -85,7 +85,6 @@ def chat(self) -> Optional[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. - owner_id = None 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.