mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-02-03 05:04:33 +03:00
Replace weird mixin Client classes with free-standing defs
This should take care of the extremely precarious subclassing order. It should also make IDEs go a lot less crazy. Documentation and code can be kept separated.
This commit is contained in:
parent
af201c12ba
commit
f639992baa
|
@ -13,12 +13,27 @@ from Telethon version 1.x to 2.0 onwards.
|
|||
User, chat and channel identifiers are now 64-bit numbers
|
||||
---------------------------------------------------------
|
||||
|
||||
`Layer 133 <https://diff.telethon.dev/?from=132&to=133>`__ changed *a lot* of
|
||||
identifiers from ``int`` to ``long``, meaning they will no longer fit in 32
|
||||
bits, and instead require 64 bits.
|
||||
`Layer 133 <https://diff.telethon.dev/?from=132&to=133>`__ changed *a lot* of identifiers from
|
||||
``int`` to ``long``, meaning they will no longer fit in 32 bits, and instead require 64 bits.
|
||||
|
||||
If you were storing these identifiers somewhere size did matter (for example,
|
||||
a database), you will need to migrate that to support the new size requirement
|
||||
of 8 bytes.
|
||||
If you were storing these identifiers somewhere size did matter (for example, a database), you
|
||||
will need to migrate that to support the new size requirement of 8 bytes.
|
||||
|
||||
For the full list of types changed, please review the above link.
|
||||
|
||||
|
||||
Many modules are now private
|
||||
----------------------------
|
||||
|
||||
There were a lot of things which were public but should not have been. From now on, you should
|
||||
only rely on things that are either publicly re-exported or defined. That is, as soon as anything
|
||||
starts with an underscore (``_``) on its name, you're acknowledging that the functionality may
|
||||
change even across minor version changes, and thus have your code break.
|
||||
|
||||
* The ``telethon.client`` module is now ``telethon._client``, meaning you should stop relying on
|
||||
anything inside of it. This includes all of the subclasses that used to exist (like ``UserMethods``).
|
||||
|
||||
TODO REVIEW self\._\w+\(
|
||||
and __signature__
|
||||
and property
|
||||
abs abc abstract
|
|
@ -1,4 +1,4 @@
|
|||
from .client.telegramclient import TelegramClient
|
||||
from ._client.telegramclient import TelegramClient
|
||||
from .network import connection
|
||||
from .tl import types, functions, custom
|
||||
from .tl.custom import Button
|
||||
|
|
|
@ -107,137 +107,40 @@ class _TakeoutClient:
|
|||
return setattr(self.__client, name, value)
|
||||
|
||||
|
||||
class AccountMethods:
|
||||
def takeout(
|
||||
self: 'TelegramClient',
|
||||
finalize: bool = True,
|
||||
*,
|
||||
contacts: bool = None,
|
||||
users: bool = None,
|
||||
chats: bool = None,
|
||||
megagroups: bool = None,
|
||||
channels: bool = None,
|
||||
files: bool = None,
|
||||
max_file_size: bool = None) -> 'TelegramClient':
|
||||
"""
|
||||
Returns a :ref:`telethon-client` which calls methods behind a takeout session.
|
||||
def takeout(
|
||||
self: 'TelegramClient',
|
||||
finalize: bool = True,
|
||||
*,
|
||||
contacts: bool = None,
|
||||
users: bool = None,
|
||||
chats: bool = None,
|
||||
megagroups: bool = None,
|
||||
channels: bool = None,
|
||||
files: bool = None,
|
||||
max_file_size: bool = None) -> 'TelegramClient':
|
||||
request_kwargs = dict(
|
||||
contacts=contacts,
|
||||
message_users=users,
|
||||
message_chats=chats,
|
||||
message_megagroups=megagroups,
|
||||
message_channels=channels,
|
||||
files=files,
|
||||
file_max_size=max_file_size
|
||||
)
|
||||
arg_specified = (arg is not None for arg in request_kwargs.values())
|
||||
|
||||
It does so by creating a proxy object over the current client through
|
||||
which making requests will use :tl:`InvokeWithTakeoutRequest` to wrap
|
||||
them. In other words, returns the current client modified so that
|
||||
requests are done as a takeout:
|
||||
if self.session.takeout_id is None or any(arg_specified):
|
||||
request = functions.account.InitTakeoutSessionRequest(
|
||||
**request_kwargs)
|
||||
else:
|
||||
request = None
|
||||
|
||||
Some of the calls made through the takeout session will have lower
|
||||
flood limits. This is useful if you want to export the data from
|
||||
conversations or mass-download media, since the rate limits will
|
||||
be lower. Only some requests will be affected, and you will need
|
||||
to adjust the `wait_time` of methods like `client.iter_messages
|
||||
<telethon.client.messages.MessageMethods.iter_messages>`.
|
||||
return _TakeoutClient(finalize, self, request)
|
||||
|
||||
By default, all parameters are `None`, and you need to enable those
|
||||
you plan to use by setting them to either `True` or `False`.
|
||||
|
||||
You should ``except errors.TakeoutInitDelayError as e``, since this
|
||||
exception will raise depending on the condition of the session. You
|
||||
can then access ``e.seconds`` to know how long you should wait for
|
||||
before calling the method again.
|
||||
|
||||
There's also a `success` property available in the takeout proxy
|
||||
object, so from the `with` body you can set the boolean result that
|
||||
will be sent back to Telegram. But if it's left `None` as by
|
||||
default, then the action is based on the `finalize` parameter. If
|
||||
it's `True` then the takeout will be finished, and if no exception
|
||||
occurred during it, then `True` will be considered as a result.
|
||||
Otherwise, the takeout will not be finished and its ID will be
|
||||
preserved for future usage as `client.session.takeout_id
|
||||
<telethon.sessions.abstract.Session.takeout_id>`.
|
||||
|
||||
Arguments
|
||||
finalize (`bool`):
|
||||
Whether the takeout session should be finalized upon
|
||||
exit or not.
|
||||
|
||||
contacts (`bool`):
|
||||
Set to `True` if you plan on downloading contacts.
|
||||
|
||||
users (`bool`):
|
||||
Set to `True` if you plan on downloading information
|
||||
from users and their private conversations with you.
|
||||
|
||||
chats (`bool`):
|
||||
Set to `True` if you plan on downloading information
|
||||
from small group chats, such as messages and media.
|
||||
|
||||
megagroups (`bool`):
|
||||
Set to `True` if you plan on downloading information
|
||||
from megagroups (channels), such as messages and media.
|
||||
|
||||
channels (`bool`):
|
||||
Set to `True` if you plan on downloading information
|
||||
from broadcast channels, such as messages and media.
|
||||
|
||||
files (`bool`):
|
||||
Set to `True` if you plan on downloading media and
|
||||
you don't only wish to export messages.
|
||||
|
||||
max_file_size (`int`):
|
||||
The maximum file size, in bytes, that you plan
|
||||
to download for each message with media.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
from telethon import errors
|
||||
|
||||
try:
|
||||
async with client.takeout() as takeout:
|
||||
await client.get_messages('me') # normal call
|
||||
await takeout.get_messages('me') # wrapped through takeout (less limits)
|
||||
|
||||
async for message in takeout.iter_messages(chat, wait_time=0):
|
||||
... # Do something with the message
|
||||
|
||||
except errors.TakeoutInitDelayError as e:
|
||||
print('Must wait', e.seconds, 'before takeout')
|
||||
"""
|
||||
request_kwargs = dict(
|
||||
contacts=contacts,
|
||||
message_users=users,
|
||||
message_chats=chats,
|
||||
message_megagroups=megagroups,
|
||||
message_channels=channels,
|
||||
files=files,
|
||||
file_max_size=max_file_size
|
||||
)
|
||||
arg_specified = (arg is not None for arg in request_kwargs.values())
|
||||
|
||||
if self.session.takeout_id is None or any(arg_specified):
|
||||
request = functions.account.InitTakeoutSessionRequest(
|
||||
**request_kwargs)
|
||||
else:
|
||||
request = None
|
||||
|
||||
return _TakeoutClient(finalize, self, request)
|
||||
|
||||
async def end_takeout(self: 'TelegramClient', success: bool) -> bool:
|
||||
"""
|
||||
Finishes the current takeout session.
|
||||
|
||||
Arguments
|
||||
success (`bool`):
|
||||
Whether the takeout completed successfully or not.
|
||||
|
||||
Returns
|
||||
`True` if the operation was successful, `False` otherwise.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
await client.end_takeout(success=False)
|
||||
"""
|
||||
try:
|
||||
async with _TakeoutClient(True, self, None) as takeout:
|
||||
takeout.success = success
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
async def end_takeout(self: 'TelegramClient', success: bool) -> bool:
|
||||
try:
|
||||
async with _TakeoutClient(True, self, None) as takeout:
|
||||
takeout.success = success
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -7,66 +7,26 @@ if typing.TYPE_CHECKING:
|
|||
from .telegramclient import TelegramClient
|
||||
|
||||
|
||||
class BotMethods:
|
||||
async def inline_query(
|
||||
self: 'TelegramClient',
|
||||
bot: 'hints.EntityLike',
|
||||
query: str,
|
||||
*,
|
||||
entity: 'hints.EntityLike' = None,
|
||||
offset: str = None,
|
||||
geo_point: 'types.GeoPoint' = None) -> custom.InlineResults:
|
||||
"""
|
||||
Makes an inline query to the specified bot (``@vote New Poll``).
|
||||
async def inline_query(
|
||||
self: 'TelegramClient',
|
||||
bot: 'hints.EntityLike',
|
||||
query: str,
|
||||
*,
|
||||
entity: 'hints.EntityLike' = None,
|
||||
offset: str = None,
|
||||
geo_point: 'types.GeoPoint' = None) -> custom.InlineResults:
|
||||
bot = await self.get_input_entity(bot)
|
||||
if entity:
|
||||
peer = await self.get_input_entity(entity)
|
||||
else:
|
||||
peer = types.InputPeerEmpty()
|
||||
|
||||
Arguments
|
||||
bot (`entity`):
|
||||
The bot entity to which the inline query should be made.
|
||||
result = await self(functions.messages.GetInlineBotResultsRequest(
|
||||
bot=bot,
|
||||
peer=peer,
|
||||
query=query,
|
||||
offset=offset or '',
|
||||
geo_point=geo_point
|
||||
))
|
||||
|
||||
query (`str`):
|
||||
The query that should be made to the bot.
|
||||
|
||||
entity (`entity`, optional):
|
||||
The entity where the inline query is being made from. Certain
|
||||
bots use this to display different results depending on where
|
||||
it's used, such as private chats, groups or channels.
|
||||
|
||||
If specified, it will also be the default entity where the
|
||||
message will be sent after clicked. Otherwise, the "empty
|
||||
peer" will be used, which some bots may not handle correctly.
|
||||
|
||||
offset (`str`, optional):
|
||||
The string offset to use for the bot.
|
||||
|
||||
geo_point (:tl:`GeoPoint`, optional)
|
||||
The geo point location information to send to the bot
|
||||
for localised results. Available under some bots.
|
||||
|
||||
Returns
|
||||
A list of `custom.InlineResult
|
||||
<telethon.tl.custom.inlineresult.InlineResult>`.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
# Make an inline query to @like
|
||||
results = await client.inline_query('like', 'Do you like Telethon?')
|
||||
|
||||
# Send the first result to some chat
|
||||
message = await results[0].click('TelethonOffTopic')
|
||||
"""
|
||||
bot = await self.get_input_entity(bot)
|
||||
if entity:
|
||||
peer = await self.get_input_entity(entity)
|
||||
else:
|
||||
peer = types.InputPeerEmpty()
|
||||
|
||||
result = await self(functions.messages.GetInlineBotResultsRequest(
|
||||
bot=bot,
|
||||
peer=peer,
|
||||
query=query,
|
||||
offset=offset or '',
|
||||
geo_point=geo_point
|
||||
))
|
||||
|
||||
return custom.InlineResults(self, result, entity=peer if entity else None)
|
||||
return custom.InlineResults(self, result, entity=peer if entity else None)
|
||||
|
|
|
@ -4,93 +4,62 @@ from .. import utils, hints
|
|||
from ..tl import types, custom
|
||||
|
||||
|
||||
class ButtonMethods:
|
||||
@staticmethod
|
||||
def build_reply_markup(
|
||||
buttons: 'typing.Optional[hints.MarkupLike]',
|
||||
inline_only: bool = False) -> 'typing.Optional[types.TypeReplyMarkup]':
|
||||
"""
|
||||
Builds a :tl:`ReplyInlineMarkup` or :tl:`ReplyKeyboardMarkup` for
|
||||
the given buttons.
|
||||
def build_reply_markup(
|
||||
buttons: 'typing.Optional[hints.MarkupLike]',
|
||||
inline_only: bool = False) -> 'typing.Optional[types.TypeReplyMarkup]':
|
||||
if buttons is None:
|
||||
return None
|
||||
|
||||
Does nothing if either no buttons are provided or the provided
|
||||
argument is already a reply markup.
|
||||
try:
|
||||
if buttons.SUBCLASS_OF_ID == 0xe2e10ef2:
|
||||
return buttons # crc32(b'ReplyMarkup'):
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
You should consider using this method if you are going to reuse
|
||||
the markup very often. Otherwise, it is not necessary.
|
||||
if not utils.is_list_like(buttons):
|
||||
buttons = [[buttons]]
|
||||
elif not buttons or not utils.is_list_like(buttons[0]):
|
||||
buttons = [buttons]
|
||||
|
||||
This method is **not** asynchronous (don't use ``await`` on it).
|
||||
is_inline = False
|
||||
is_normal = False
|
||||
resize = None
|
||||
single_use = None
|
||||
selective = None
|
||||
|
||||
Arguments
|
||||
buttons (`hints.MarkupLike`):
|
||||
The button, list of buttons, array of buttons or markup
|
||||
to convert into a markup.
|
||||
rows = []
|
||||
for row in buttons:
|
||||
current = []
|
||||
for button in row:
|
||||
if isinstance(button, custom.Button):
|
||||
if button.resize is not None:
|
||||
resize = button.resize
|
||||
if button.single_use is not None:
|
||||
single_use = button.single_use
|
||||
if button.selective is not None:
|
||||
selective = button.selective
|
||||
|
||||
inline_only (`bool`, optional):
|
||||
Whether the buttons **must** be inline buttons only or not.
|
||||
button = button.button
|
||||
elif isinstance(button, custom.MessageButton):
|
||||
button = button.button
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
inline = custom.Button._is_inline(button)
|
||||
is_inline |= inline
|
||||
is_normal |= not inline
|
||||
|
||||
from telethon import Button
|
||||
if button.SUBCLASS_OF_ID == 0xbad74a3:
|
||||
# 0xbad74a3 == crc32(b'KeyboardButton')
|
||||
current.append(button)
|
||||
|
||||
markup = client.build_reply_markup(Button.inline('hi'))
|
||||
# later
|
||||
await client.send_message(chat, 'click me', buttons=markup)
|
||||
"""
|
||||
if buttons is None:
|
||||
return None
|
||||
if current:
|
||||
rows.append(types.KeyboardButtonRow(current))
|
||||
|
||||
try:
|
||||
if buttons.SUBCLASS_OF_ID == 0xe2e10ef2:
|
||||
return buttons # crc32(b'ReplyMarkup'):
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
if not utils.is_list_like(buttons):
|
||||
buttons = [[buttons]]
|
||||
elif not buttons or not utils.is_list_like(buttons[0]):
|
||||
buttons = [buttons]
|
||||
|
||||
is_inline = False
|
||||
is_normal = False
|
||||
resize = None
|
||||
single_use = None
|
||||
selective = None
|
||||
|
||||
rows = []
|
||||
for row in buttons:
|
||||
current = []
|
||||
for button in row:
|
||||
if isinstance(button, custom.Button):
|
||||
if button.resize is not None:
|
||||
resize = button.resize
|
||||
if button.single_use is not None:
|
||||
single_use = button.single_use
|
||||
if button.selective is not None:
|
||||
selective = button.selective
|
||||
|
||||
button = button.button
|
||||
elif isinstance(button, custom.MessageButton):
|
||||
button = button.button
|
||||
|
||||
inline = custom.Button._is_inline(button)
|
||||
is_inline |= inline
|
||||
is_normal |= not inline
|
||||
|
||||
if button.SUBCLASS_OF_ID == 0xbad74a3:
|
||||
# 0xbad74a3 == crc32(b'KeyboardButton')
|
||||
current.append(button)
|
||||
|
||||
if current:
|
||||
rows.append(types.KeyboardButtonRow(current))
|
||||
|
||||
if inline_only and is_normal:
|
||||
raise ValueError('You cannot use non-inline buttons here')
|
||||
elif is_inline == is_normal and is_normal:
|
||||
raise ValueError('You cannot mix inline with normal buttons')
|
||||
elif is_inline:
|
||||
return types.ReplyInlineMarkup(rows)
|
||||
# elif is_normal:
|
||||
return types.ReplyKeyboardMarkup(
|
||||
rows, resize=resize, single_use=single_use, selective=selective)
|
||||
if inline_only and is_normal:
|
||||
raise ValueError('You cannot use non-inline buttons here')
|
||||
elif is_inline == is_normal and is_normal:
|
||||
raise ValueError('You cannot mix inline with normal buttons')
|
||||
elif is_inline:
|
||||
return types.ReplyInlineMarkup(rows)
|
||||
# elif is_normal:
|
||||
return types.ReplyKeyboardMarkup(
|
||||
rows, resize=resize, single_use=single_use, selective=selective)
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -136,471 +136,139 @@ class _DraftsIter(RequestIter):
|
|||
return []
|
||||
|
||||
|
||||
class DialogMethods:
|
||||
def iter_dialogs(
|
||||
self: 'TelegramClient',
|
||||
limit: float = None,
|
||||
*,
|
||||
offset_date: 'hints.DateLike' = None,
|
||||
offset_id: int = 0,
|
||||
offset_peer: 'hints.EntityLike' = types.InputPeerEmpty(),
|
||||
ignore_pinned: bool = False,
|
||||
ignore_migrated: bool = False,
|
||||
folder: int = None,
|
||||
archived: bool = None
|
||||
) -> _DialogsIter:
|
||||
if archived is not None:
|
||||
folder = 1 if archived else 0
|
||||
|
||||
# region Public methods
|
||||
return _DialogsIter(
|
||||
self,
|
||||
limit,
|
||||
offset_date=offset_date,
|
||||
offset_id=offset_id,
|
||||
offset_peer=offset_peer,
|
||||
ignore_pinned=ignore_pinned,
|
||||
ignore_migrated=ignore_migrated,
|
||||
folder=folder
|
||||
)
|
||||
|
||||
def iter_dialogs(
|
||||
self: 'TelegramClient',
|
||||
limit: float = None,
|
||||
*,
|
||||
offset_date: 'hints.DateLike' = None,
|
||||
offset_id: int = 0,
|
||||
offset_peer: 'hints.EntityLike' = types.InputPeerEmpty(),
|
||||
ignore_pinned: bool = False,
|
||||
ignore_migrated: bool = False,
|
||||
folder: int = None,
|
||||
archived: bool = None
|
||||
) -> _DialogsIter:
|
||||
"""
|
||||
Iterator over the dialogs (open conversations/subscribed channels).
|
||||
async def get_dialogs(self: 'TelegramClient', *args, **kwargs) -> 'hints.TotalList':
|
||||
return await self.iter_dialogs(*args, **kwargs).collect()
|
||||
|
||||
The order is the same as the one seen in official applications
|
||||
(first pinned, them from those with the most recent message to
|
||||
those with the oldest message).
|
||||
|
||||
Arguments
|
||||
limit (`int` | `None`):
|
||||
How many dialogs to be retrieved as maximum. Can be set to
|
||||
`None` to retrieve all dialogs. Note that this may take
|
||||
whole minutes if you have hundreds of dialogs, as Telegram
|
||||
will tell the library to slow down through a
|
||||
``FloodWaitError``.
|
||||
def iter_drafts(
|
||||
self: 'TelegramClient',
|
||||
entity: 'hints.EntitiesLike' = None
|
||||
) -> _DraftsIter:
|
||||
if entity and not utils.is_list_like(entity):
|
||||
entity = (entity,)
|
||||
|
||||
offset_date (`datetime`, optional):
|
||||
The offset date to be used.
|
||||
# TODO Passing a limit here makes no sense
|
||||
return _DraftsIter(self, None, entities=entity)
|
||||
|
||||
offset_id (`int`, optional):
|
||||
The message ID to be used as an offset.
|
||||
async def get_drafts(
|
||||
self: 'TelegramClient',
|
||||
entity: 'hints.EntitiesLike' = None
|
||||
) -> 'hints.TotalList':
|
||||
items = await self.iter_drafts(entity).collect()
|
||||
if not entity or utils.is_list_like(entity):
|
||||
return items
|
||||
else:
|
||||
return items[0]
|
||||
|
||||
offset_peer (:tl:`InputPeer`, optional):
|
||||
The peer to be used as an offset.
|
||||
async def edit_folder(
|
||||
self: 'TelegramClient',
|
||||
entity: 'hints.EntitiesLike' = None,
|
||||
folder: typing.Union[int, typing.Sequence[int]] = None,
|
||||
*,
|
||||
unpack=None
|
||||
) -> types.Updates:
|
||||
if (entity is None) == (unpack is None):
|
||||
raise ValueError('You can only set either entities or unpack, not both')
|
||||
|
||||
ignore_pinned (`bool`, optional):
|
||||
Whether pinned dialogs should be ignored or not.
|
||||
When set to `True`, these won't be yielded at all.
|
||||
if unpack is not None:
|
||||
return await self(functions.folders.DeleteFolderRequest(
|
||||
folder_id=unpack
|
||||
))
|
||||
|
||||
ignore_migrated (`bool`, optional):
|
||||
Whether :tl:`Chat` that have ``migrated_to`` a :tl:`Channel`
|
||||
should be included or not. By default all the chats in your
|
||||
dialogs are returned, but setting this to `True` will ignore
|
||||
(i.e. skip) them in the same way official applications do.
|
||||
if not utils.is_list_like(entity):
|
||||
entities = [await self.get_input_entity(entity)]
|
||||
else:
|
||||
entities = await asyncio.gather(
|
||||
*(self.get_input_entity(x) for x in entity))
|
||||
|
||||
folder (`int`, optional):
|
||||
The folder from which the dialogs should be retrieved.
|
||||
if folder is None:
|
||||
raise ValueError('You must specify a folder')
|
||||
elif not utils.is_list_like(folder):
|
||||
folder = [folder] * len(entities)
|
||||
elif len(entities) != len(folder):
|
||||
raise ValueError('Number of folders does not match number of entities')
|
||||
|
||||
If left unspecified, all dialogs (including those from
|
||||
folders) will be returned.
|
||||
return await self(functions.folders.EditPeerFoldersRequest([
|
||||
types.InputFolderPeer(x, folder_id=y)
|
||||
for x, y in zip(entities, folder)
|
||||
]))
|
||||
|
||||
If set to ``0``, all dialogs that don't belong to any
|
||||
folder will be returned.
|
||||
async def delete_dialog(
|
||||
self: 'TelegramClient',
|
||||
entity: 'hints.EntityLike',
|
||||
*,
|
||||
revoke: bool = False
|
||||
):
|
||||
# If we have enough information (`Dialog.delete` gives it to us),
|
||||
# then we know we don't have to kick ourselves in deactivated chats.
|
||||
if isinstance(entity, types.Chat):
|
||||
deactivated = entity.deactivated
|
||||
else:
|
||||
deactivated = False
|
||||
|
||||
If set to a folder number like ``1``, only those from
|
||||
said folder will be returned.
|
||||
entity = await self.get_input_entity(entity)
|
||||
ty = helpers._entity_type(entity)
|
||||
if ty == helpers._EntityType.CHANNEL:
|
||||
return await self(functions.channels.LeaveChannelRequest(entity))
|
||||
|
||||
By default Telegram assigns the folder ID ``1`` to
|
||||
archived chats, so you should use that if you need
|
||||
to fetch the archived dialogs.
|
||||
|
||||
archived (`bool`, optional):
|
||||
Alias for `folder`. If unspecified, all will be returned,
|
||||
`False` implies ``folder=0`` and `True` implies ``folder=1``.
|
||||
Yields
|
||||
Instances of `Dialog <telethon.tl.custom.dialog.Dialog>`.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
# Print all dialog IDs and the title, nicely formatted
|
||||
async for dialog in client.iter_dialogs():
|
||||
print('{:>14}: {}'.format(dialog.id, dialog.title))
|
||||
"""
|
||||
if archived is not None:
|
||||
folder = 1 if archived else 0
|
||||
|
||||
return _DialogsIter(
|
||||
self,
|
||||
limit,
|
||||
offset_date=offset_date,
|
||||
offset_id=offset_id,
|
||||
offset_peer=offset_peer,
|
||||
ignore_pinned=ignore_pinned,
|
||||
ignore_migrated=ignore_migrated,
|
||||
folder=folder
|
||||
)
|
||||
|
||||
async def get_dialogs(self: 'TelegramClient', *args, **kwargs) -> 'hints.TotalList':
|
||||
"""
|
||||
Same as `iter_dialogs()`, but returns a
|
||||
`TotalList <telethon.helpers.TotalList>` instead.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
# Get all open conversation, print the title of the first
|
||||
dialogs = await client.get_dialogs()
|
||||
first = dialogs[0]
|
||||
print(first.title)
|
||||
|
||||
# Use the dialog somewhere else
|
||||
await client.send_message(first, 'hi')
|
||||
|
||||
# Getting only non-archived dialogs (both equivalent)
|
||||
non_archived = await client.get_dialogs(folder=0)
|
||||
non_archived = await client.get_dialogs(archived=False)
|
||||
|
||||
# Getting only archived dialogs (both equivalent)
|
||||
archived = await client.get_dialogs(folder=1)
|
||||
archived = await client.get_dialogs(archived=True)
|
||||
"""
|
||||
return await self.iter_dialogs(*args, **kwargs).collect()
|
||||
|
||||
get_dialogs.__signature__ = inspect.signature(iter_dialogs)
|
||||
|
||||
def iter_drafts(
|
||||
self: 'TelegramClient',
|
||||
entity: 'hints.EntitiesLike' = None
|
||||
) -> _DraftsIter:
|
||||
"""
|
||||
Iterator over draft messages.
|
||||
|
||||
The order is unspecified.
|
||||
|
||||
Arguments
|
||||
entity (`hints.EntitiesLike`, optional):
|
||||
The entity or entities for which to fetch the draft messages.
|
||||
If left unspecified, all draft messages will be returned.
|
||||
|
||||
Yields
|
||||
Instances of `Draft <telethon.tl.custom.draft.Draft>`.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
# Clear all drafts
|
||||
async for draft in client.get_drafts():
|
||||
await draft.delete()
|
||||
|
||||
# Getting the drafts with 'bot1' and 'bot2'
|
||||
async for draft in client.iter_drafts(['bot1', 'bot2']):
|
||||
print(draft.text)
|
||||
"""
|
||||
if entity and not utils.is_list_like(entity):
|
||||
entity = (entity,)
|
||||
|
||||
# TODO Passing a limit here makes no sense
|
||||
return _DraftsIter(self, None, entities=entity)
|
||||
|
||||
async def get_drafts(
|
||||
self: 'TelegramClient',
|
||||
entity: 'hints.EntitiesLike' = None
|
||||
) -> 'hints.TotalList':
|
||||
"""
|
||||
Same as `iter_drafts()`, but returns a list instead.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
# Get drafts, print the text of the first
|
||||
drafts = await client.get_drafts()
|
||||
print(drafts[0].text)
|
||||
|
||||
# Get the draft in your chat
|
||||
draft = await client.get_drafts('me')
|
||||
print(drafts.text)
|
||||
"""
|
||||
items = await self.iter_drafts(entity).collect()
|
||||
if not entity or utils.is_list_like(entity):
|
||||
return items
|
||||
else:
|
||||
return items[0]
|
||||
|
||||
async def edit_folder(
|
||||
self: 'TelegramClient',
|
||||
entity: 'hints.EntitiesLike' = None,
|
||||
folder: typing.Union[int, typing.Sequence[int]] = None,
|
||||
*,
|
||||
unpack=None
|
||||
) -> types.Updates:
|
||||
"""
|
||||
Edits the folder used by one or more dialogs to archive them.
|
||||
|
||||
Arguments
|
||||
entity (entities):
|
||||
The entity or list of entities to move to the desired
|
||||
archive folder.
|
||||
|
||||
folder (`int`):
|
||||
The folder to which the dialog should be archived to.
|
||||
|
||||
If you want to "archive" a dialog, use ``folder=1``.
|
||||
|
||||
If you want to "un-archive" it, use ``folder=0``.
|
||||
|
||||
You may also pass a list with the same length as
|
||||
`entities` if you want to control where each entity
|
||||
will go.
|
||||
|
||||
unpack (`int`, optional):
|
||||
If you want to unpack an archived folder, set this
|
||||
parameter to the folder number that you want to
|
||||
delete.
|
||||
|
||||
When you unpack a folder, all the dialogs inside are
|
||||
moved to the folder number 0.
|
||||
|
||||
You can only use this parameter if the other two
|
||||
are not set.
|
||||
|
||||
Returns
|
||||
The :tl:`Updates` object that the request produces.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
# Archiving the first 5 dialogs
|
||||
dialogs = await client.get_dialogs(5)
|
||||
await client.edit_folder(dialogs, 1)
|
||||
|
||||
# Un-archiving the third dialog (archiving to folder 0)
|
||||
await client.edit_folder(dialog[2], 0)
|
||||
|
||||
# Moving the first dialog to folder 0 and the second to 1
|
||||
dialogs = await client.get_dialogs(2)
|
||||
await client.edit_folder(dialogs, [0, 1])
|
||||
|
||||
# Un-archiving all dialogs
|
||||
await client.edit_folder(unpack=1)
|
||||
"""
|
||||
if (entity is None) == (unpack is None):
|
||||
raise ValueError('You can only set either entities or unpack, not both')
|
||||
|
||||
if unpack is not None:
|
||||
return await self(functions.folders.DeleteFolderRequest(
|
||||
folder_id=unpack
|
||||
if ty == helpers._EntityType.CHAT and not deactivated:
|
||||
try:
|
||||
result = await self(functions.messages.DeleteChatUserRequest(
|
||||
entity.chat_id, types.InputUserSelf(), revoke_history=revoke
|
||||
))
|
||||
|
||||
if not utils.is_list_like(entity):
|
||||
entities = [await self.get_input_entity(entity)]
|
||||
else:
|
||||
entities = await asyncio.gather(
|
||||
*(self.get_input_entity(x) for x in entity))
|
||||
|
||||
if folder is None:
|
||||
raise ValueError('You must specify a folder')
|
||||
elif not utils.is_list_like(folder):
|
||||
folder = [folder] * len(entities)
|
||||
elif len(entities) != len(folder):
|
||||
raise ValueError('Number of folders does not match number of entities')
|
||||
|
||||
return await self(functions.folders.EditPeerFoldersRequest([
|
||||
types.InputFolderPeer(x, folder_id=y)
|
||||
for x, y in zip(entities, folder)
|
||||
]))
|
||||
|
||||
async def delete_dialog(
|
||||
self: 'TelegramClient',
|
||||
entity: 'hints.EntityLike',
|
||||
*,
|
||||
revoke: bool = False
|
||||
):
|
||||
"""
|
||||
Deletes a dialog (leaves a chat or channel).
|
||||
|
||||
This method can be used as a user and as a bot. However,
|
||||
bots will only be able to use it to leave groups and channels
|
||||
(trying to delete a private conversation will do nothing).
|
||||
|
||||
See also `Dialog.delete() <telethon.tl.custom.dialog.Dialog.delete>`.
|
||||
|
||||
Arguments
|
||||
entity (entities):
|
||||
The entity of the dialog to delete. If it's a chat or
|
||||
channel, you will leave it. Note that the chat itself
|
||||
is not deleted, only the dialog, because you left it.
|
||||
|
||||
revoke (`bool`, optional):
|
||||
On private chats, you may revoke the messages from
|
||||
the other peer too. By default, it's `False`. Set
|
||||
it to `True` to delete the history for both.
|
||||
|
||||
This makes no difference for bot accounts, who can
|
||||
only leave groups and channels.
|
||||
|
||||
Returns
|
||||
The :tl:`Updates` object that the request produces,
|
||||
or nothing for private conversations.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
# Deleting the first dialog
|
||||
dialogs = await client.get_dialogs(5)
|
||||
await client.delete_dialog(dialogs[0])
|
||||
|
||||
# Leaving a channel by username
|
||||
await client.delete_dialog('username')
|
||||
"""
|
||||
# If we have enough information (`Dialog.delete` gives it to us),
|
||||
# then we know we don't have to kick ourselves in deactivated chats.
|
||||
if isinstance(entity, types.Chat):
|
||||
deactivated = entity.deactivated
|
||||
else:
|
||||
deactivated = False
|
||||
|
||||
entity = await self.get_input_entity(entity)
|
||||
ty = helpers._entity_type(entity)
|
||||
if ty == helpers._EntityType.CHANNEL:
|
||||
return await self(functions.channels.LeaveChannelRequest(entity))
|
||||
|
||||
if ty == helpers._EntityType.CHAT and not deactivated:
|
||||
try:
|
||||
result = await self(functions.messages.DeleteChatUserRequest(
|
||||
entity.chat_id, types.InputUserSelf(), revoke_history=revoke
|
||||
))
|
||||
except errors.PeerIdInvalidError:
|
||||
# Happens if we didn't have the deactivated information
|
||||
result = None
|
||||
else:
|
||||
except errors.PeerIdInvalidError:
|
||||
# Happens if we didn't have the deactivated information
|
||||
result = None
|
||||
else:
|
||||
result = None
|
||||
|
||||
if not await self.is_bot():
|
||||
await self(functions.messages.DeleteHistoryRequest(entity, 0, revoke=revoke))
|
||||
if not await self.is_bot():
|
||||
await self(functions.messages.DeleteHistoryRequest(entity, 0, revoke=revoke))
|
||||
|
||||
return result
|
||||
return result
|
||||
|
||||
def conversation(
|
||||
self: 'TelegramClient',
|
||||
entity: 'hints.EntityLike',
|
||||
*,
|
||||
timeout: float = 60,
|
||||
total_timeout: float = None,
|
||||
max_messages: int = 100,
|
||||
exclusive: bool = True,
|
||||
replies_are_responses: bool = True) -> custom.Conversation:
|
||||
"""
|
||||
Creates a `Conversation <telethon.tl.custom.conversation.Conversation>`
|
||||
with the given entity.
|
||||
def conversation(
|
||||
self: 'TelegramClient',
|
||||
entity: 'hints.EntityLike',
|
||||
*,
|
||||
timeout: float = 60,
|
||||
total_timeout: float = None,
|
||||
max_messages: int = 100,
|
||||
exclusive: bool = True,
|
||||
replies_are_responses: bool = True) -> custom.Conversation:
|
||||
return custom.Conversation(
|
||||
self,
|
||||
entity,
|
||||
timeout=timeout,
|
||||
total_timeout=total_timeout,
|
||||
max_messages=max_messages,
|
||||
exclusive=exclusive,
|
||||
replies_are_responses=replies_are_responses
|
||||
|
||||
.. note::
|
||||
|
||||
This Conversation API has certain shortcomings, such as lacking
|
||||
persistence, poor interaction with other event handlers, and
|
||||
overcomplicated usage for anything beyond the simplest case.
|
||||
|
||||
If you plan to interact with a bot without handlers, this works
|
||||
fine, but when running a bot yourself, you may instead prefer
|
||||
to follow the advice from https://stackoverflow.com/a/62246569/.
|
||||
|
||||
This is not the same as just sending a message to create a "dialog"
|
||||
with them, but rather a way to easily send messages and await for
|
||||
responses or other reactions. Refer to its documentation for more.
|
||||
|
||||
Arguments
|
||||
entity (`entity`):
|
||||
The entity with which a new conversation should be opened.
|
||||
|
||||
timeout (`int` | `float`, optional):
|
||||
The default timeout (in seconds) *per action* to be used. You
|
||||
may also override this timeout on a per-method basis. By
|
||||
default each action can take up to 60 seconds (the value of
|
||||
this timeout).
|
||||
|
||||
total_timeout (`int` | `float`, optional):
|
||||
The total timeout (in seconds) to use for the whole
|
||||
conversation. This takes priority over per-action
|
||||
timeouts. After these many seconds pass, subsequent
|
||||
actions will result in ``asyncio.TimeoutError``.
|
||||
|
||||
max_messages (`int`, optional):
|
||||
The maximum amount of messages this conversation will
|
||||
remember. After these many messages arrive in the
|
||||
specified chat, subsequent actions will result in
|
||||
``ValueError``.
|
||||
|
||||
exclusive (`bool`, optional):
|
||||
By default, conversations are exclusive within a single
|
||||
chat. That means that while a conversation is open in a
|
||||
chat, you can't open another one in the same chat, unless
|
||||
you disable this flag.
|
||||
|
||||
If you try opening an exclusive conversation for
|
||||
a chat where it's already open, it will raise
|
||||
``AlreadyInConversationError``.
|
||||
|
||||
replies_are_responses (`bool`, optional):
|
||||
Whether replies should be treated as responses or not.
|
||||
|
||||
If the setting is enabled, calls to `conv.get_response
|
||||
<telethon.tl.custom.conversation.Conversation.get_response>`
|
||||
and a subsequent call to `conv.get_reply
|
||||
<telethon.tl.custom.conversation.Conversation.get_reply>`
|
||||
will return different messages, otherwise they may return
|
||||
the same message.
|
||||
|
||||
Consider the following scenario with one outgoing message,
|
||||
1, and two incoming messages, the second one replying::
|
||||
|
||||
Hello! <1
|
||||
2> (reply to 1) Hi!
|
||||
3> (reply to 1) How are you?
|
||||
|
||||
And the following code:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
async with client.conversation(chat) as conv:
|
||||
msg1 = await conv.send_message('Hello!')
|
||||
msg2 = await conv.get_response()
|
||||
msg3 = await conv.get_reply()
|
||||
|
||||
With the setting enabled, ``msg2`` will be ``'Hi!'`` and
|
||||
``msg3`` be ``'How are you?'`` since replies are also
|
||||
responses, and a response was already returned.
|
||||
|
||||
With the setting disabled, both ``msg2`` and ``msg3`` will
|
||||
be ``'Hi!'`` since one is a response and also a reply.
|
||||
|
||||
Returns
|
||||
A `Conversation <telethon.tl.custom.conversation.Conversation>`.
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
# <you> denotes outgoing messages you sent
|
||||
# <usr> denotes incoming response messages
|
||||
with bot.conversation(chat) as conv:
|
||||
# <you> Hi!
|
||||
conv.send_message('Hi!')
|
||||
|
||||
# <usr> Hello!
|
||||
hello = conv.get_response()
|
||||
|
||||
# <you> Please tell me your name
|
||||
conv.send_message('Please tell me your name')
|
||||
|
||||
# <usr> ?
|
||||
name = conv.get_response().raw_text
|
||||
|
||||
while not any(x.isalpha() for x in name):
|
||||
# <you> Your name didn't have any letters! Try again
|
||||
conv.send_message("Your name didn't have any letters! Try again")
|
||||
|
||||
# <usr> Human
|
||||
name = conv.get_response().raw_text
|
||||
|
||||
# <you> Thanks Human!
|
||||
conv.send_message('Thanks {}!'.format(name))
|
||||
"""
|
||||
return custom.Conversation(
|
||||
self,
|
||||
entity,
|
||||
timeout=timeout,
|
||||
total_timeout=total_timeout,
|
||||
max_messages=max_messages,
|
||||
exclusive=exclusive,
|
||||
replies_are_responses=replies_are_responses
|
||||
|
||||
)
|
||||
|
||||
# endregion
|
||||
)
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -9,220 +9,212 @@ if typing.TYPE_CHECKING:
|
|||
from .telegramclient import TelegramClient
|
||||
|
||||
|
||||
class MessageParseMethods:
|
||||
def get_parse_mode(self: 'TelegramClient'):
|
||||
"""
|
||||
This property is the default parse mode used when sending messages.
|
||||
Defaults to `telethon.extensions.markdown`. It will always
|
||||
be either `None` or an object with ``parse`` and ``unparse``
|
||||
methods.
|
||||
|
||||
# region Public properties
|
||||
When setting a different value it should be one of:
|
||||
|
||||
@property
|
||||
def parse_mode(self: 'TelegramClient'):
|
||||
"""
|
||||
This property is the default parse mode used when sending messages.
|
||||
Defaults to `telethon.extensions.markdown`. It will always
|
||||
be either `None` or an object with ``parse`` and ``unparse``
|
||||
methods.
|
||||
* Object with ``parse`` and ``unparse`` methods.
|
||||
* A ``callable`` to act as the parse method.
|
||||
* A `str` indicating the ``parse_mode``. For Markdown ``'md'``
|
||||
or ``'markdown'`` may be used. For HTML, ``'htm'`` or ``'html'``
|
||||
may be used.
|
||||
|
||||
When setting a different value it should be one of:
|
||||
The ``parse`` method should be a function accepting a single
|
||||
parameter, the text to parse, and returning a tuple consisting
|
||||
of ``(parsed message str, [MessageEntity instances])``.
|
||||
|
||||
* Object with ``parse`` and ``unparse`` methods.
|
||||
* A ``callable`` to act as the parse method.
|
||||
* A `str` indicating the ``parse_mode``. For Markdown ``'md'``
|
||||
or ``'markdown'`` may be used. For HTML, ``'htm'`` or ``'html'``
|
||||
may be used.
|
||||
The ``unparse`` method should be the inverse of ``parse`` such
|
||||
that ``assert text == unparse(*parse(text))``.
|
||||
|
||||
The ``parse`` method should be a function accepting a single
|
||||
parameter, the text to parse, and returning a tuple consisting
|
||||
of ``(parsed message str, [MessageEntity instances])``.
|
||||
See :tl:`MessageEntity` for allowed message entities.
|
||||
|
||||
The ``unparse`` method should be the inverse of ``parse`` such
|
||||
that ``assert text == unparse(*parse(text))``.
|
||||
Example
|
||||
.. code-block:: python
|
||||
|
||||
See :tl:`MessageEntity` for allowed message entities.
|
||||
# Disabling default formatting
|
||||
client.parse_mode = None
|
||||
|
||||
Example
|
||||
.. code-block:: python
|
||||
# Enabling HTML as the default format
|
||||
client.parse_mode = 'html'
|
||||
"""
|
||||
return self._parse_mode
|
||||
|
||||
# Disabling default formatting
|
||||
client.parse_mode = None
|
||||
def set_parse_mode(self: 'TelegramClient', mode: str):
|
||||
self._parse_mode = utils.sanitize_parse_mode(mode)
|
||||
|
||||
# Enabling HTML as the default format
|
||||
client.parse_mode = 'html'
|
||||
"""
|
||||
return self._parse_mode
|
||||
# endregion
|
||||
|
||||
@parse_mode.setter
|
||||
def parse_mode(self: 'TelegramClient', mode: str):
|
||||
self._parse_mode = utils.sanitize_parse_mode(mode)
|
||||
# region Private methods
|
||||
|
||||
# endregion
|
||||
async def _replace_with_mention(self: 'TelegramClient', entities, i, user):
|
||||
"""
|
||||
Helper method to replace ``entities[i]`` to mention ``user``,
|
||||
or do nothing if it can't be found.
|
||||
"""
|
||||
try:
|
||||
entities[i] = types.InputMessageEntityMentionName(
|
||||
entities[i].offset, entities[i].length,
|
||||
await self.get_input_entity(user)
|
||||
)
|
||||
return True
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
|
||||
# region Private methods
|
||||
async def _parse_message_text(self: 'TelegramClient', message, parse_mode):
|
||||
"""
|
||||
Returns a (parsed message, entities) tuple depending on ``parse_mode``.
|
||||
"""
|
||||
if parse_mode == ():
|
||||
parse_mode = self._parse_mode
|
||||
else:
|
||||
parse_mode = utils.sanitize_parse_mode(parse_mode)
|
||||
|
||||
async def _replace_with_mention(self: 'TelegramClient', entities, i, user):
|
||||
"""
|
||||
Helper method to replace ``entities[i]`` to mention ``user``,
|
||||
or do nothing if it can't be found.
|
||||
"""
|
||||
try:
|
||||
entities[i] = types.InputMessageEntityMentionName(
|
||||
entities[i].offset, entities[i].length,
|
||||
await self.get_input_entity(user)
|
||||
)
|
||||
return True
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
if not parse_mode:
|
||||
return message, []
|
||||
|
||||
async def _parse_message_text(self: 'TelegramClient', message, parse_mode):
|
||||
"""
|
||||
Returns a (parsed message, entities) tuple depending on ``parse_mode``.
|
||||
"""
|
||||
if parse_mode == ():
|
||||
parse_mode = self._parse_mode
|
||||
else:
|
||||
parse_mode = utils.sanitize_parse_mode(parse_mode)
|
||||
original_message = message
|
||||
message, msg_entities = parse_mode.parse(message)
|
||||
if original_message and not message and not msg_entities:
|
||||
raise ValueError("Failed to parse message")
|
||||
|
||||
if not parse_mode:
|
||||
return message, []
|
||||
|
||||
original_message = message
|
||||
message, msg_entities = parse_mode.parse(message)
|
||||
if original_message and not message and not msg_entities:
|
||||
raise ValueError("Failed to parse message")
|
||||
|
||||
for i in reversed(range(len(msg_entities))):
|
||||
e = msg_entities[i]
|
||||
if isinstance(e, types.MessageEntityTextUrl):
|
||||
m = re.match(r'^@|\+|tg://user\?id=(\d+)', e.url)
|
||||
if m:
|
||||
user = int(m.group(1)) if m.group(1) else e.url
|
||||
is_mention = await self._replace_with_mention(msg_entities, i, user)
|
||||
if not is_mention:
|
||||
del msg_entities[i]
|
||||
elif isinstance(e, (types.MessageEntityMentionName,
|
||||
types.InputMessageEntityMentionName)):
|
||||
is_mention = await self._replace_with_mention(msg_entities, i, e.user_id)
|
||||
for i in reversed(range(len(msg_entities))):
|
||||
e = msg_entities[i]
|
||||
if isinstance(e, types.MessageEntityTextUrl):
|
||||
m = re.match(r'^@|\+|tg://user\?id=(\d+)', e.url)
|
||||
if m:
|
||||
user = int(m.group(1)) if m.group(1) else e.url
|
||||
is_mention = await self._replace_with_mention(msg_entities, i, user)
|
||||
if not is_mention:
|
||||
del msg_entities[i]
|
||||
elif isinstance(e, (types.MessageEntityMentionName,
|
||||
types.InputMessageEntityMentionName)):
|
||||
is_mention = await self._replace_with_mention(msg_entities, i, e.user_id)
|
||||
if not is_mention:
|
||||
del msg_entities[i]
|
||||
|
||||
return message, msg_entities
|
||||
return message, msg_entities
|
||||
|
||||
def _get_response_message(self: 'TelegramClient', request, result, input_chat):
|
||||
"""
|
||||
Extracts the response message known a request and Update result.
|
||||
The request may also be the ID of the message to match.
|
||||
def _get_response_message(self: 'TelegramClient', request, result, input_chat):
|
||||
"""
|
||||
Extracts the response message known a request and Update result.
|
||||
The request may also be the ID of the message to match.
|
||||
|
||||
If ``request is None`` this method returns ``{id: message}``.
|
||||
If ``request is None`` this method returns ``{id: message}``.
|
||||
|
||||
If ``request.random_id`` is a list, this method returns a list too.
|
||||
"""
|
||||
if isinstance(result, types.UpdateShort):
|
||||
updates = [result.update]
|
||||
entities = {}
|
||||
elif isinstance(result, (types.Updates, types.UpdatesCombined)):
|
||||
updates = result.updates
|
||||
entities = {utils.get_peer_id(x): x
|
||||
for x in
|
||||
itertools.chain(result.users, result.chats)}
|
||||
else:
|
||||
return None
|
||||
If ``request.random_id`` is a list, this method returns a list too.
|
||||
"""
|
||||
if isinstance(result, types.UpdateShort):
|
||||
updates = [result.update]
|
||||
entities = {}
|
||||
elif isinstance(result, (types.Updates, types.UpdatesCombined)):
|
||||
updates = result.updates
|
||||
entities = {utils.get_peer_id(x): x
|
||||
for x in
|
||||
itertools.chain(result.users, result.chats)}
|
||||
else:
|
||||
return None
|
||||
|
||||
random_to_id = {}
|
||||
id_to_message = {}
|
||||
for update in updates:
|
||||
if isinstance(update, types.UpdateMessageID):
|
||||
random_to_id[update.random_id] = update.id
|
||||
random_to_id = {}
|
||||
id_to_message = {}
|
||||
for update in updates:
|
||||
if isinstance(update, types.UpdateMessageID):
|
||||
random_to_id[update.random_id] = update.id
|
||||
|
||||
elif isinstance(update, (
|
||||
types.UpdateNewChannelMessage, types.UpdateNewMessage)):
|
||||
update.message._finish_init(self, entities, input_chat)
|
||||
elif isinstance(update, (
|
||||
types.UpdateNewChannelMessage, types.UpdateNewMessage)):
|
||||
update.message._finish_init(self, entities, input_chat)
|
||||
|
||||
# Pinning a message with `updatePinnedMessage` seems to
|
||||
# always produce a service message we can't map so return
|
||||
# it directly. The same happens for kicking users.
|
||||
#
|
||||
# It could also be a list (e.g. when sending albums).
|
||||
#
|
||||
# TODO this method is getting messier and messier as time goes on
|
||||
if hasattr(request, 'random_id') or utils.is_list_like(request):
|
||||
id_to_message[update.message.id] = update.message
|
||||
else:
|
||||
return update.message
|
||||
|
||||
elif (isinstance(update, types.UpdateEditMessage)
|
||||
and helpers._entity_type(request.peer) != helpers._EntityType.CHANNEL):
|
||||
update.message._finish_init(self, entities, input_chat)
|
||||
|
||||
# Live locations use `sendMedia` but Telegram responds with
|
||||
# `updateEditMessage`, which means we won't have `id` field.
|
||||
if hasattr(request, 'random_id'):
|
||||
id_to_message[update.message.id] = update.message
|
||||
elif request.id == update.message.id:
|
||||
return update.message
|
||||
|
||||
elif (isinstance(update, types.UpdateEditChannelMessage)
|
||||
and utils.get_peer_id(request.peer) ==
|
||||
utils.get_peer_id(update.message.peer_id)):
|
||||
if request.id == update.message.id:
|
||||
update.message._finish_init(self, entities, input_chat)
|
||||
return update.message
|
||||
|
||||
elif isinstance(update, types.UpdateNewScheduledMessage):
|
||||
update.message._finish_init(self, entities, input_chat)
|
||||
# Scheduled IDs may collide with normal IDs. However, for a
|
||||
# single request there *shouldn't* be a mix between "some
|
||||
# scheduled and some not".
|
||||
id_to_message[update.message.id] = update.message
|
||||
|
||||
elif isinstance(update, types.UpdateMessagePoll):
|
||||
if request.media.poll.id == update.poll_id:
|
||||
m = types.Message(
|
||||
id=request.id,
|
||||
peer_id=utils.get_peer(request.peer),
|
||||
media=types.MessageMediaPoll(
|
||||
poll=update.poll,
|
||||
results=update.results
|
||||
)
|
||||
)
|
||||
m._finish_init(self, entities, input_chat)
|
||||
return m
|
||||
|
||||
if request is None:
|
||||
return id_to_message
|
||||
|
||||
random_id = request if isinstance(request, (int, list)) else getattr(request, 'random_id', None)
|
||||
if random_id is None:
|
||||
# Can happen when pinning a message does not actually produce a service message.
|
||||
self._log[__name__].warning(
|
||||
'No random_id in %s to map to, returning None message for %s', request, result)
|
||||
return None
|
||||
|
||||
if not utils.is_list_like(random_id):
|
||||
msg = id_to_message.get(random_to_id.get(random_id))
|
||||
|
||||
if not msg:
|
||||
self._log[__name__].warning(
|
||||
'Request %s had missing message mapping %s', request, result)
|
||||
|
||||
return msg
|
||||
|
||||
try:
|
||||
return [id_to_message[random_to_id[rnd]] for rnd in random_id]
|
||||
except KeyError:
|
||||
# Sometimes forwards fail (`MESSAGE_ID_INVALID` if a message gets
|
||||
# deleted or `WORKER_BUSY_TOO_LONG_RETRY` if there are issues at
|
||||
# Telegram), in which case we get some "missing" message mappings.
|
||||
# Log them with the hope that we can better work around them.
|
||||
# Pinning a message with `updatePinnedMessage` seems to
|
||||
# always produce a service message we can't map so return
|
||||
# it directly. The same happens for kicking users.
|
||||
#
|
||||
# This also happens when trying to forward messages that can't
|
||||
# be forwarded because they don't exist (0, service, deleted)
|
||||
# among others which could be (like deleted or existing).
|
||||
# It could also be a list (e.g. when sending albums).
|
||||
#
|
||||
# TODO this method is getting messier and messier as time goes on
|
||||
if hasattr(request, 'random_id') or utils.is_list_like(request):
|
||||
id_to_message[update.message.id] = update.message
|
||||
else:
|
||||
return update.message
|
||||
|
||||
elif (isinstance(update, types.UpdateEditMessage)
|
||||
and helpers._entity_type(request.peer) != helpers._EntityType.CHANNEL):
|
||||
update.message._finish_init(self, entities, input_chat)
|
||||
|
||||
# Live locations use `sendMedia` but Telegram responds with
|
||||
# `updateEditMessage`, which means we won't have `id` field.
|
||||
if hasattr(request, 'random_id'):
|
||||
id_to_message[update.message.id] = update.message
|
||||
elif request.id == update.message.id:
|
||||
return update.message
|
||||
|
||||
elif (isinstance(update, types.UpdateEditChannelMessage)
|
||||
and utils.get_peer_id(request.peer) ==
|
||||
utils.get_peer_id(update.message.peer_id)):
|
||||
if request.id == update.message.id:
|
||||
update.message._finish_init(self, entities, input_chat)
|
||||
return update.message
|
||||
|
||||
elif isinstance(update, types.UpdateNewScheduledMessage):
|
||||
update.message._finish_init(self, entities, input_chat)
|
||||
# Scheduled IDs may collide with normal IDs. However, for a
|
||||
# single request there *shouldn't* be a mix between "some
|
||||
# scheduled and some not".
|
||||
id_to_message[update.message.id] = update.message
|
||||
|
||||
elif isinstance(update, types.UpdateMessagePoll):
|
||||
if request.media.poll.id == update.poll_id:
|
||||
m = types.Message(
|
||||
id=request.id,
|
||||
peer_id=utils.get_peer(request.peer),
|
||||
media=types.MessageMediaPoll(
|
||||
poll=update.poll,
|
||||
results=update.results
|
||||
)
|
||||
)
|
||||
m._finish_init(self, entities, input_chat)
|
||||
return m
|
||||
|
||||
if request is None:
|
||||
return id_to_message
|
||||
|
||||
random_id = request if isinstance(request, (int, list)) else getattr(request, 'random_id', None)
|
||||
if random_id is None:
|
||||
# Can happen when pinning a message does not actually produce a service message.
|
||||
self._log[__name__].warning(
|
||||
'No random_id in %s to map to, returning None message for %s', request, result)
|
||||
return None
|
||||
|
||||
if not utils.is_list_like(random_id):
|
||||
msg = id_to_message.get(random_to_id.get(random_id))
|
||||
|
||||
if not msg:
|
||||
self._log[__name__].warning(
|
||||
'Request %s had missing message mappings %s', request, result)
|
||||
'Request %s had missing message mapping %s', request, result)
|
||||
|
||||
return [
|
||||
id_to_message.get(random_to_id[rnd])
|
||||
if rnd in random_to_id
|
||||
else None
|
||||
for rnd in random_id
|
||||
]
|
||||
return msg
|
||||
|
||||
# endregion
|
||||
try:
|
||||
return [id_to_message[random_to_id[rnd]] for rnd in random_id]
|
||||
except KeyError:
|
||||
# Sometimes forwards fail (`MESSAGE_ID_INVALID` if a message gets
|
||||
# deleted or `WORKER_BUSY_TOO_LONG_RETRY` if there are issues at
|
||||
# Telegram), in which case we get some "missing" message mappings.
|
||||
# Log them with the hope that we can better work around them.
|
||||
#
|
||||
# This also happens when trying to forward messages that can't
|
||||
# be forwarded because they don't exist (0, service, deleted)
|
||||
# among others which could be (like deleted or existing).
|
||||
self._log[__name__].warning(
|
||||
'Request %s had missing message mappings %s', request, result)
|
||||
|
||||
return [
|
||||
id_to_message.get(random_to_id[rnd])
|
||||
if rnd in random_to_id
|
||||
else None
|
||||
for rnd in random_id
|
||||
]
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user