diff --git a/telethon/events/chataction.py b/telethon/events/chataction.py index be377cd6..4e2abe1e 100644 --- a/telethon/events/chataction.py +++ b/telethon/events/chataction.py @@ -204,9 +204,7 @@ class ChatAction(EventBuilder): if isinstance(self._pinned_message, int) and await self.input_chat: r = await self._client(functions.channels.GetMessagesRequest( - self._input_chat, [ - types.InputMessageID(self._pinned_message) - ] + self._input_chat, [self._pinned_message] )) try: self._pinned_message = next( diff --git a/telethon/events/common.py b/telethon/events/common.py index eab3e2ff..6a4277dd 100644 --- a/telethon/events/common.py +++ b/telethon/events/common.py @@ -118,15 +118,11 @@ class EventCommon(abc.ABC): try: if isinstance(chat, types.InputPeerChannel): result = await self._client( - functions.channels.GetMessagesRequest(chat, [ - types.InputMessageID(msg_id) - ]) + functions.channels.GetMessagesRequest(chat, [msg_id]) ) else: result = await self._client( - functions.messages.GetMessagesRequest([ - types.InputMessageID(msg_id) - ]) + functions.messages.GetMessagesRequest([msg_id]) ) except RPCError: return None, None diff --git a/telethon/events/messageread.py b/telethon/events/messageread.py index 9c1d77cb..f29cbefc 100644 --- a/telethon/events/messageread.py +++ b/telethon/events/messageread.py @@ -101,16 +101,14 @@ class MessageRead(EventBuilder): if not chat: self._messages = [] elif isinstance(chat, types.InputPeerChannel): - ids = [types.InputMessageID(x) for x in self._message_ids] self._messages =\ await self._client(functions.channels.GetMessagesRequest( - chat, ids + chat, self._message_ids )).messages else: - ids = [types.InputMessageID(x) for x in self._message_ids] self._messages = \ await self._client(functions.messages.GetMessagesRequest( - ids + self._message_ids )).messages return self._messages diff --git a/telethon/events/newmessage.py b/telethon/events/newmessage.py index 2ae7a184..e410c997 100644 --- a/telethon/events/newmessage.py +++ b/telethon/events/newmessage.py @@ -297,13 +297,11 @@ class NewMessage(EventBuilder): if self._reply_message is None: if isinstance(await self.input_chat, types.InputPeerChannel): r = await self._client(functions.channels.GetMessagesRequest( - await self.input_chat, [ - types.InputMessageID(self.message.reply_to_msg_id) - ] + await self.input_chat, [self.message.reply_to_msg_id] )) else: r = await self._client(functions.messages.GetMessagesRequest( - [types.InputMessageID(self.message.reply_to_msg_id)] + [self.message.reply_to_msg_id] )) if not isinstance(r, types.messages.MessagesNotModified): self._reply_message = r.messages[0] diff --git a/telethon/sessions/sqlite.py b/telethon/sessions/sqlite.py index 7c594fdf..3bad4a28 100644 --- a/telethon/sessions/sqlite.py +++ b/telethon/sessions/sqlite.py @@ -17,7 +17,7 @@ CURRENT_VERSION = 3 # database version class SQLiteSession(MemorySession): """This session contains the required information to login into your - Telegram account. NEVER give the saved JSON file to anyone, since + Telegram account. NEVER give the saved session file to anyone, since they would gain instant access to all your messages and contacts. If you think the session has been compromised, close all the sessions diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index 65f7dc88..2d019952 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -9,7 +9,8 @@ from .crypto import rsa from .errors import ( RPCError, BrokenAuthKeyError, ServerError, FloodWaitError, FloodTestPhoneWaitError, TypeNotFoundError, UnauthorizedError, - PhoneMigrateError, NetworkMigrateError, UserMigrateError, AuthKeyError + PhoneMigrateError, NetworkMigrateError, UserMigrateError, AuthKeyError, + RpcCallFailError ) from .network import authenticator, MtProtoSender, Connection, ConnectionMode from .sessions import Session, SQLiteSession @@ -544,9 +545,9 @@ class TelegramBareClient: await self._reconnect(new_dc=e.new_dc) return await self._invoke(call_receive, retry, *requests) - except ServerError as e: + except (ServerError, RpcCallFailError) as e: # Telegram is having some issues, just retry - __log__.error('Telegram servers are having internal errors %s', e) + __log__.warning('Telegram is having internal issues: %s', e) except (FloodWaitError, FloodTestPhoneWaitError) as e: __log__.warning('Request invoked too often, wait %ds', e.seconds) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 1e24398d..eb9b6644 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -58,7 +58,7 @@ from .tl.functions.messages import ( SendMessageRequest, GetChatsRequest, GetAllDraftsRequest, CheckChatInviteRequest, ReadMentionsRequest, SendMultiMediaRequest, UploadMediaRequest, EditMessageRequest, GetFullChatRequest, - ForwardMessagesRequest + ForwardMessagesRequest, SearchRequest ) from .tl.functions import channels @@ -780,9 +780,13 @@ class TelegramClient(TelegramBareClient): if isinstance(message, Message): if (message.media and not isinstance(message.media, MessageMediaWebPage)): - return await self.send_file(entity, message.media) + return await self.send_file(entity, message.media, + caption=message.message, + entities=message.entities) - if utils.get_peer_id(entity) == utils.get_peer_id(message.to_id): + if reply_to is not None: + reply_id = self._get_message_id(reply_to) + elif utils.get_peer_id(entity) == utils.get_peer_id(message.to_id): reply_id = message.reply_to_msg_id else: reply_id = None @@ -879,20 +883,26 @@ class TelegramClient(TelegramBareClient): result = [id_to_message[random_to_id[rnd]] for rnd in req.random_id] return result[0] if single else result - async def edit_message(self, entity, message_id, message=None, + async def edit_message(self, entity, message=None, text=None, parse_mode='md', link_preview=True): """ Edits the given message ID (to change its contents or disable preview). Args: - entity (`entity`): - From which chat to edit the message. + entity (`entity` | :tl:`Message`): + From which chat to edit the message. This can also be + the message to be edited, and the entity will be inferred + from it, so the next parameter will be assumed to be the + message text. - message_id (`str`): - The ID of the message (or ``Message`` itself) to be edited. + message (`int` | :tl:`Message` | `str`): + The ID of the message (or :tl:`Message` itself) to be edited. + If the `entity` was a :tl:`Message`, then this message will be + treated as the new text. - message (`str`, optional): - The new text of the message. + text (`str`, optional): + The new text of the message. Does nothing if the `entity` + was a :tl:`Message`. parse_mode (`str`, optional): Can be 'md' or 'markdown' for markdown-like parsing (default), @@ -903,6 +913,21 @@ class TelegramClient(TelegramBareClient): link_preview (`bool`, optional): Should the link preview be shown? + Examples: + + >>> async def main(): + ... client = await TelegramClient(...).start() + ... message = await client.send_message('username', 'hello') + ... + ... await client.edit_message('username', message, 'hello!') + ... # or + ... await client.edit_message('username', message.id, 'Hello') + ... # or + ... await client.edit_message(message, 'Hello!') + ... + >>> loop = ... + >>> loop.run_until_complete(main()) + Raises: ``MessageAuthorRequiredError`` if you're not the author of the message but try editing it anyway. @@ -913,12 +938,16 @@ class TelegramClient(TelegramBareClient): Returns: The edited :tl:`Message`. """ - message, msg_entities =\ - await self._parse_message_text(message, parse_mode) + if isinstance(entity, Message): + text = message # Shift the parameters to the right + message = entity + entity = entity.to_id + + text, msg_entities = await self._parse_message_text(text, parse_mode) request = EditMessageRequest( peer=await self.get_input_entity(entity), - id=self._get_message_id(message_id), - message=message, + id=self._get_message_id(message), + message=text, no_webpage=not link_preview, entities=msg_entities ) @@ -967,10 +996,14 @@ class TelegramClient(TelegramBareClient): async def iter_messages(self, entity, limit=20, offset_date=None, offset_id=0, max_id=0, min_id=0, add_offset=0, + search=None, filter=None, from_user=None, batch_size=100, wait_time=None, _total=None): """ Iterator over the message history for the specified entity. + If either `search`, `filter` or `from_user` are provided, + :tl:`messages.Search` will be used instead of :tl:`messages.getHistory`. + Args: entity (`entity`): The entity from whom to retrieve the message history. @@ -1002,6 +1035,17 @@ class TelegramClient(TelegramBareClient): Additional message offset (all of the specified offsets + this offset = older messages). + search (`str`): + The string to be used as a search query. + + filter (:tl:`MessagesFilter` | `type`): + The filter to use when returning messages. For instance, + :tl:`InputMessagesFilterPhotos` would yield only messages + containing photos. + + from_user (`entity`): + Only messages from this user will be returned. + batch_size (`int`): Messages will be returned in chunks of this size (100 is the maximum). While it makes no sense to modify this value, @@ -1033,15 +1077,37 @@ class TelegramClient(TelegramBareClient): """ entity = await self.get_input_entity(entity) limit = float('inf') if limit is None else int(limit) + if search is not None or filter or from_user: + request = SearchRequest( + peer=entity, + q=search or '', + filter=filter() if isinstance(filter, type) else filter, + min_date=None, + max_date=offset_date, + offset_id=offset_id, + add_offset=add_offset, + limit=1, + max_id=max_id, + min_id=min_id, + from_id=self.get_input_entity(from_user) if from_user else None + ) + else: + request = GetHistoryRequest( + peer=entity, + limit=1, + offset_date=offset_date, + offset_id=offset_id, + min_id=min_id, + max_id=max_id, + add_offset=add_offset, + hash=0 + ) + if limit == 0: if not _total: return # No messages, but we still need to know the total message count - result = await self(GetHistoryRequest( - peer=entity, limit=1, - offset_date=None, offset_id=0, max_id=0, min_id=0, - add_offset=0, hash=0 - )) + result = await self(request) _total[0] = getattr(result, 'count', len(result.messages)) return @@ -1052,17 +1118,8 @@ class TelegramClient(TelegramBareClient): batch_size = min(max(batch_size, 1), 100) while have < limit: # Telegram has a hard limit of 100 - real_limit = min(limit - have, batch_size) - r = await self(GetHistoryRequest( - peer=entity, - limit=real_limit, - offset_date=offset_date, - offset_id=offset_id, - max_id=max_id, - min_id=min_id, - add_offset=add_offset, - hash=0 - )) + request.limit = min(limit - have, batch_size) + r = await self(request) if _total: _total[0] = getattr(r, 'count', len(r.messages)) @@ -1097,11 +1154,15 @@ class TelegramClient(TelegramBareClient): yield message have += 1 - if len(r.messages) < real_limit: + if len(r.messages) < request.limit: break - offset_id = r.messages[-1].id - offset_date = r.messages[-1].date + request.offset_id = r.messages[-1].id + if isinstance(request, GetHistoryRequest): + request.offset_date = r.messages[-1].date + else: + request.max_date = r.messages[-1].date + await asyncio.sleep(wait_time) async def get_messages(self, *args, **kwargs): @@ -1479,8 +1540,14 @@ class TelegramClient(TelegramBareClient): entity = await self.get_input_entity(entity) reply_to = self._get_message_id(reply_to) - caption, msg_entities =\ - await self._parse_message_text(caption, parse_mode) + + # Not document since it's subject to change. + # Needed when a Message is passed to send_message and it has media. + if 'entities' in kwargs: + msg_entities = kwargs['entities'] + else: + caption, msg_entities =\ + await self._parse_message_text(caption, parse_mode) if not isinstance(file, (str, bytes, io.IOBase)): # The user may pass a Message containing media (or the media, @@ -2481,7 +2548,11 @@ class TelegramClient(TelegramBareClient): If in the end the access hash required for the peer was not found, a ValueError will be raised. Returns: - :tl:`InputPeerUser`, :tl:`InputPeerChat` or :tl:`InputPeerChannel`. + :tl:`InputPeerUser`, :tl:`InputPeerChat` or :tl:`InputPeerChannel` + or :tl:`InputPeerSelf` if the parameter is ``'me'`` or ``'self'``. + + If you need to get the ID of yourself, you should use + `get_me` with ``input_peer=True``) instead. """ if peer in ('me', 'self'): return InputPeerSelf() diff --git a/telethon/utils.py b/telethon/utils.py index ee489f90..0c6693b1 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -25,7 +25,7 @@ from .tl.types import ( InputPhotoEmpty, FileLocation, ChatPhotoEmpty, UserProfilePhotoEmpty, FileLocationUnavailable, InputMediaUploadedDocument, ChannelFull, InputMediaUploadedPhoto, DocumentAttributeFilename, photos, - TopPeer, InputNotifyPeer + TopPeer, InputNotifyPeer, InputMessageID ) from .tl.types.contacts import ResolvedPeer @@ -255,8 +255,12 @@ def get_input_media(media, is_photo=False): it will be treated as an :tl:`InputMediaUploadedPhoto`. """ try: - if media.SUBCLASS_OF_ID == 0xfaf846f4: # crc32(b'InputMedia'): + if media.SUBCLASS_OF_ID == 0xfaf846f4: # crc32(b'InputMedia') return media + elif media.SUBCLASS_OF_ID == 0x846363e0: # crc32(b'InputPhoto') + return InputMediaPhoto(media) + elif media.SUBCLASS_OF_ID == 0xf33fdb68: # crc32(b'InputDocument') + return InputMediaDocument(media) except AttributeError: _raise_cast_fail(media, 'InputMedia') @@ -333,6 +337,21 @@ def get_input_media(media, is_photo=False): _raise_cast_fail(media, 'InputMedia') +def get_input_message(message): + """Similar to :meth:`get_input_peer`, but for input messages.""" + try: + if isinstance(message, int): # This case is really common too + return InputMessageID(message) + elif message.SUBCLASS_OF_ID == 0x54b6bcc5: # crc32(b'InputMessage'): + return message + elif message.SUBCLASS_OF_ID == 0x790009e3: # crc32(b'Message'): + return InputMessageID(message.id) + except AttributeError: + pass + + _raise_cast_fail(message, 'InputMedia') + + def is_image(file): """ Returns ``True`` if the file extension looks like an image file to Telegram. diff --git a/telethon_generator/generators/tlobject.py b/telethon_generator/generators/tlobject.py index 4cc157d0..921db3af 100644 --- a/telethon_generator/generators/tlobject.py +++ b/telethon_generator/generators/tlobject.py @@ -18,7 +18,8 @@ AUTO_CASTS = { 'InputChannel': 'utils.get_input_channel(await client.get_input_entity({}))', 'InputUser': 'utils.get_input_user(await client.get_input_entity({}))', 'InputMedia': 'utils.get_input_media({})', - 'InputPhoto': 'utils.get_input_photo({})' + 'InputPhoto': 'utils.get_input_photo({})', + 'InputMessage': 'utils.get_input_message({})' } BASE_TYPES = ('string', 'bytes', 'int', 'long', 'int128',