From 7763939e7db79ad47dcbfc55df552509fbe2e7d9 Mon Sep 17 00:00:00 2001 From: Shrimadhav U K Date: Fri, 9 Jul 2021 23:17:13 +0530 Subject: [PATCH 001/256] Update to layer 130 (#3098) --- telethon_generator/data/api.tl | 24 +++++++++++++++++++----- telethon_generator/data/errors.csv | 4 +++- telethon_generator/data/methods.csv | 4 +++- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/telethon_generator/data/api.tl b/telethon_generator/data/api.tl index 1db76ca1..8b070368 100644 --- a/telethon_generator/data/api.tl +++ b/telethon_generator/data/api.tl @@ -579,8 +579,8 @@ keyboardButtonRequestPoll#bbc7515d flags:# quiz:flags.0?Bool text:string = Keybo keyboardButtonRow#77608b83 buttons:Vector = KeyboardButtonRow; replyKeyboardHide#a03e5b85 flags:# selective:flags.2?true = ReplyMarkup; -replyKeyboardForceReply#f4108aa0 flags:# single_use:flags.1?true selective:flags.2?true = ReplyMarkup; -replyKeyboardMarkup#3502758c flags:# resize:flags.0?true single_use:flags.1?true selective:flags.2?true rows:Vector = ReplyMarkup; +replyKeyboardForceReply#86b40b08 flags:# single_use:flags.1?true selective:flags.2?true placeholder:flags.3?string = ReplyMarkup; +replyKeyboardMarkup#85dd99d1 flags:# resize:flags.0?true single_use:flags.1?true selective:flags.2?true rows:Vector placeholder:flags.3?string = ReplyMarkup; replyInlineMarkup#48a30254 rows:Vector = ReplyMarkup; messageEntityUnknown#bb92ba95 offset:int length:int = MessageEntity; @@ -1250,6 +1250,16 @@ groupCallParticipantVideoSourceGroup#dcb118b7 semantics:string sources:Vector = GroupCallParticipantVideo; +stickers.suggestedShortName#85fea03f short_name:string = stickers.SuggestedShortName; + +botCommandScopeDefault#2f6cb2ab = BotCommandScope; +botCommandScopeUsers#3c4f04d8 = BotCommandScope; +botCommandScopeChats#6fe1a881 = BotCommandScope; +botCommandScopeChatAdmins#b9aa606a = BotCommandScope; +botCommandScopePeer#db9d897d peer:InputPeer = BotCommandScope; +botCommandScopePeerAdmins#3fd863d1 peer:InputPeer = BotCommandScope; +botCommandScopePeerUser#a1321f3 peer:InputPeer user_id:InputUser = BotCommandScope; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1596,7 +1606,9 @@ channels.convertToGigagroup#b290c69 channel:InputChannel = Updates; bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON; bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool; -bots.setBotCommands#805d46f6 commands:Vector = Bool; +bots.setBotCommands#517165a scope:BotCommandScope lang_code:string commands:Vector = Bool; +bots.resetBotCommands#3d8de0f9 scope:BotCommandScope lang_code:string = Bool; +bots.getBotCommands#e34c0dd6 scope:BotCommandScope lang_code:string = Vector; payments.getPaymentForm#8a333c8d flags:# peer:InputPeer msg_id:int theme_params:flags.0?DataJSON = payments.PaymentForm; payments.getPaymentReceipt#2478d1cc peer:InputPeer msg_id:int = payments.PaymentReceipt; @@ -1606,11 +1618,13 @@ payments.getSavedInfo#227d824b = payments.SavedInfo; payments.clearSavedInfo#d83d70c1 flags:# credentials:flags.0?true info:flags.1?true = Bool; payments.getBankCardData#2e79d779 number:string = payments.BankCardData; -stickers.createStickerSet#f1036780 flags:# masks:flags.0?true animated:flags.1?true user_id:InputUser title:string short_name:string thumb:flags.2?InputDocument stickers:Vector = messages.StickerSet; +stickers.createStickerSet#9021ab67 flags:# masks:flags.0?true animated:flags.1?true user_id:InputUser title:string short_name:string thumb:flags.2?InputDocument stickers:Vector software:flags.3?string = messages.StickerSet; stickers.removeStickerFromSet#f7760f51 sticker:InputDocument = messages.StickerSet; stickers.changeStickerPosition#ffb6d4ca sticker:InputDocument position:int = messages.StickerSet; stickers.addStickerToSet#8653febe stickerset:InputStickerSet sticker:InputStickerSetItem = messages.StickerSet; stickers.setStickerSetThumb#9a364e30 stickerset:InputStickerSet thumb:InputDocument = messages.StickerSet; +stickers.checkShortName#284b3639 short_name:string = Bool; +stickers.suggestShortName#4dafc503 title:string = stickers.SuggestedShortName; phone.getCallConfig#55451fa9 = DataJSON; phone.requestCall#42ff96ed flags:# video:flags.0?true user_id:InputUser random_id:int g_a_hash:bytes protocol:PhoneCallProtocol = phone.PhoneCall; @@ -1656,4 +1670,4 @@ stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel stats.getMessagePublicForwards#5630281b channel:InputChannel msg_id:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats; -// LAYER 129 +// LAYER 130 diff --git a/telethon_generator/data/errors.csv b/telethon_generator/data/errors.csv index 85571513..1fef87da 100644 --- a/telethon_generator/data/errors.csv +++ b/telethon_generator/data/errors.csv @@ -309,6 +309,7 @@ SESSION_REVOKED,401,"The authorization has been invalidated, because of the user SESSION_TOO_FRESH_X,400,The session logged in too recently and {seconds} seconds must pass before calling the method SHA256_HASH_INVALID,400,The provided SHA256 hash is invalid SHORTNAME_OCCUPY_FAILED,400,An error occurred when trying to register the short-name used for the sticker pack. Try a different name +SHORT_NAME_OCCUPIED,400, SLOWMODE_WAIT_X,420,A wait of {seconds} seconds is required before sending another message in this chat SRP_ID_INVALID,400, START_PARAM_EMPTY,400,The start parameter is empty @@ -335,6 +336,7 @@ TAKEOUT_INVALID,400,The takeout session has been invalidated by another data exp TAKEOUT_REQUIRED,400,You must initialize a takeout request first TEMP_AUTH_KEY_EMPTY,400,No temporary auth key provided TIMEOUT,500,A timeout occurred while fetching data from the worker +TITLE_INVALID,400, THEME_INVALID,400,Theme invalid THEME_MIME_INVALID,400,"You cannot create this theme, the mime-type is invalid" TMP_PASSWORD_DISABLED,400,The temporary password is disabled @@ -388,4 +390,4 @@ WEBDOCUMENT_URL_INVALID,400,The given URL cannot be used WEBPAGE_CURL_FAILED,400,Failure while fetching the webpage with cURL WEBPAGE_MEDIA_EMPTY,400,Webpage media empty WORKER_BUSY_TOO_LONG_RETRY,500,Telegram workers are too busy to respond immediately -YOU_BLOCKED_USER,400,You blocked this user \ No newline at end of file +YOU_BLOCKED_USER,400,You blocked this user diff --git a/telethon_generator/data/methods.csv b/telethon_generator/data/methods.csv index 2e96877d..00dc3ca5 100644 --- a/telethon_generator/data/methods.csv +++ b/telethon_generator/data/methods.csv @@ -335,9 +335,11 @@ stats.getMegagroupStats,user,CHAT_ADMIN_REQUIRED MEGAGROUP_REQUIRED STATS_MIGRAT stats.loadAsyncGraph,user,GRAPH_INVALID_RELOAD GRAPH_OUTDATED_RELOAD stickers.addStickerToSet,bot,BOT_MISSING STICKERSET_INVALID STICKER_PNG_NOPNG STICKER_TGS_NOTGS stickers.changeStickerPosition,bot,BOT_MISSING STICKER_INVALID +stickers.checkShortName,both,SHORT_NAME_OCCUPIED stickers.createStickerSet,bot,BOT_MISSING PACK_SHORT_NAME_INVALID PACK_SHORT_NAME_OCCUPIED PEER_ID_INVALID SHORTNAME_OCCUPY_FAILED STICKERS_EMPTY STICKER_EMOJI_INVALID STICKER_FILE_INVALID STICKER_PNG_DIMENSIONS STICKER_PNG_NOPNG STICKER_TGS_NOTGS STICKER_THUMB_PNG_NOPNG STICKER_THUMB_TGS_NOTGS USER_ID_INVALID stickers.removeStickerFromSet,bot,BOT_MISSING STICKER_INVALID stickers.setStickerSetThumb,bot,STICKER_THUMB_PNG_NOPNG STICKER_THUMB_TGS_NOTGS +stickers.suggestShortName,both,TITLE_INVALID updates.getChannelDifference,both,CHANNEL_INVALID CHANNEL_PRIVATE CHANNEL_PUBLIC_GROUP_NA HISTORY_GET_FAILED PERSISTENT_TIMESTAMP_EMPTY PERSISTENT_TIMESTAMP_INVALID PERSISTENT_TIMESTAMP_OUTDATED RANGES_INVALID TIMEOUT updates.getDifference,both,AUTH_KEY_PERM_EMPTY CDN_METHOD_INVALID DATE_EMPTY NEED_MEMBER_INVALID PERSISTENT_TIMESTAMP_EMPTY PERSISTENT_TIMESTAMP_INVALID SESSION_PASSWORD_NEEDED STORE_INVALID_SCALAR_TYPE TIMEOUT updates.getState,both,AUTH_KEY_DUPLICATED MSGID_DECREASE_RETRY SESSION_PASSWORD_NEEDED TIMEOUT @@ -351,4 +353,4 @@ upload.saveBigFilePart,both,FILE_PARTS_INVALID FILE_PART_EMPTY FILE_PART_INVALID upload.saveFilePart,both,FILE_PART_EMPTY FILE_PART_INVALID INPUT_FETCH_FAIL SESSION_PASSWORD_NEEDED users.getFullUser,both,TIMEOUT USER_ID_INVALID users.getUsers,both,AUTH_KEY_PERM_EMPTY MEMBER_NO_LOCATION NEED_MEMBER_INVALID SESSION_PASSWORD_NEEDED TIMEOUT -users.setSecureValueErrors,bot, \ No newline at end of file +users.setSecureValueErrors,bot, From ecfc6ae87dedc5633182d5b0ba5022b340501537 Mon Sep 17 00:00:00 2001 From: Shrimadhav U K Date: Fri, 9 Jul 2021 23:20:47 +0530 Subject: [PATCH 002/256] Add pm_oneside parameter in pin_message (#3095) --- telethon/client/messages.py | 13 ++++++++++--- telethon/tl/custom/message.py | 4 ++-- telethon_generator/data/errors.csv | 1 + telethon_generator/data/methods.csv | 2 +- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/telethon/client/messages.py b/telethon/client/messages.py index 6bdb7f5d..31a954f1 100644 --- a/telethon/client/messages.py +++ b/telethon/client/messages.py @@ -1322,7 +1322,8 @@ class MessageMethods: entity: 'hints.EntityLike', message: 'typing.Optional[hints.MessageIDLike]', *, - notify: bool = False + notify: bool = False, + pm_oneside: bool = False ): """ Pins a message in a chat. @@ -1342,6 +1343,11 @@ class MessageMethods: notify (`bool`, optional): Whether the pin should notify people or not. + + pm_oneside (`bool`, optional): + Whether the message should be pinned for everyone or not. + By default it has the opposite behaviour of official clients, + and it will pin the message for both sides, in private chats. Example .. code-block:: python @@ -1350,7 +1356,7 @@ class MessageMethods: message = await client.send_message(chat, 'Pinotifying is fun!') await client.pin_message(chat, message, notify=True) """ - return await self._pin(entity, message, unpin=False, notify=notify) + return await self._pin(entity, message, unpin=False, notify=notify, pm_oneside=pm_oneside) async def unpin_message( self: 'TelegramClient', @@ -1382,7 +1388,7 @@ class MessageMethods: """ return await self._pin(entity, message, unpin=True, notify=notify) - async def _pin(self, entity, message, *, unpin, notify=False): + async def _pin(self, entity, message, *, unpin, notify=False, pm_oneside=False): message = utils.get_message_id(message) or 0 entity = await self.get_input_entity(entity) if message <= 0: # old behaviour accepted negative IDs to unpin @@ -1394,6 +1400,7 @@ class MessageMethods: id=message, silent=not notify, unpin=unpin, + pm_oneside=pm_oneside ) result = await self(request) diff --git a/telethon/tl/custom/message.py b/telethon/tl/custom/message.py index 3d069bf7..b55f9065 100644 --- a/telethon/tl/custom/message.py +++ b/telethon/tl/custom/message.py @@ -1038,7 +1038,7 @@ class Message(ChatGetter, SenderGetter, TLObject): await self._client.send_read_acknowledge( await self.get_input_chat(), max_id=self.id) - async def pin(self, *, notify=False): + async def pin(self, *, notify=False, pm_oneside=False): """ Pins the message. Shorthand for `telethon.client.messages.MessageMethods.pin_message` @@ -1049,7 +1049,7 @@ class Message(ChatGetter, SenderGetter, TLObject): # That or figure out a way to always set it directly. if self._client: return await self._client.pin_message( - await self.get_input_chat(), self.id, notify=notify) + await self.get_input_chat(), self.id, notify=notify, pm_oneside=pm_oneside) async def unpin(self): """ diff --git a/telethon_generator/data/errors.csv b/telethon_generator/data/errors.csv index 1fef87da..7e176924 100644 --- a/telethon_generator/data/errors.csv +++ b/telethon_generator/data/errors.csv @@ -27,6 +27,7 @@ BANK_CARD_NUMBER_INVALID,400,Incorrect credit card number BASE_PORT_LOC_INVALID,400,Base port location invalid BANNED_RIGHTS_INVALID,400,"You cannot use that set of permissions in this request, i.e. restricting view_messages as a default" BOTS_TOO_MUCH,400,There are too many bots in this chat/channel +BOT_ONESIDE_NOT_AVAIL,400, BOT_CHANNELS_NA,400,Bots can't edit admin privileges BOT_COMMAND_DESCRIPTION_INVALID,400,"The command description was empty, too long or had invalid characters used" BOT_COMMAND_INVALID,400, diff --git a/telethon_generator/data/methods.csv b/telethon_generator/data/methods.csv index 00dc3ca5..82bce6ed 100644 --- a/telethon_generator/data/methods.csv +++ b/telethon_generator/data/methods.csv @@ -296,7 +296,7 @@ messages.toggleStickerSets,user, messages.uninstallStickerSet,user,STICKERSET_INVALID messages.updateDialogFilter,user, messages.updateDialogFiltersOrder,user, -messages.updatePinnedMessage,both, +messages.updatePinnedMessage,both,BOT_ONESIDE_NOT_AVAIL messages.uploadEncryptedFile,user, messages.uploadMedia,both,BOT_MISSING MEDIA_INVALID PEER_ID_INVALID payments.clearSavedInfo,user, From 1e09e133e31cffaaf6fdd696b124c1fe3ab387d0 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 9 Jul 2021 19:43:50 +0200 Subject: [PATCH 003/256] Document new known RPC errors Courtesy of #3097. --- telethon_generator/data/errors.csv | 3 +++ telethon_generator/data/methods.csv | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/telethon_generator/data/errors.csv b/telethon_generator/data/errors.csv index 7e176924..26d4c774 100644 --- a/telethon_generator/data/errors.csv +++ b/telethon_generator/data/errors.csv @@ -91,6 +91,7 @@ CONNECTION_SYSTEM_EMPTY,400,Connection system empty CONNECTION_SYSTEM_LANG_CODE_EMPTY,400,The system language string was empty during connection CONTACT_ID_INVALID,400,The provided contact ID is invalid CONTACT_NAME_EMPTY,400,The provided contact name cannot be empty +CURRENCY_TOTAL_AMOUNT_INVALID,400, DATA_INVALID,400,Encrypted data invalid DATA_JSON_INVALID,400,The provided JSON data is invalid DATE_EMPTY,400,Date empty @@ -118,6 +119,7 @@ EXTERNAL_URL_INVALID,400,External URL invalid FIELD_NAME_EMPTY,400,The field with the name FIELD_NAME is missing FIELD_NAME_INVALID,400,The field with the name FIELD_NAME is invalid FILEREF_UPGRADE_NEEDED,406,The file reference needs to be refreshed before being used again +FILE_CONTENT_TYPE_INVALID,400, FILE_ID_INVALID,400,"The provided file id is invalid. Make sure all parameters are present, have the correct type and are not empty (ID, access hash, file reference, thumb size ...)" FILE_MIGRATE_X,303,The file to be accessed is currently stored in DC {new_dc} FILE_PARTS_INVALID,400,The number of file parts is invalid @@ -145,6 +147,7 @@ GIF_CONTENT_TYPE_INVALID,400, GIF_ID_INVALID,400,The provided GIF ID is invalid GRAPH_INVALID_RELOAD,400, GRAPH_OUTDATED_RELOAD,400,"Data can't be used for the channel statistics, graphs outdated" +GROUPCALL_ALREADY_DISCARDED,400, GROUPCALL_FORBIDDEN,403, GROUPCALL_JOIN_MISSING,400, GROUPCALL_SSRC_DUPLICATE_MUCH,400, diff --git a/telethon_generator/data/methods.csv b/telethon_generator/data/methods.csv index 82bce6ed..72900127 100644 --- a/telethon_generator/data/methods.csv +++ b/telethon_generator/data/methods.csv @@ -275,7 +275,7 @@ messages.sendEncrypted,user,CHAT_ID_INVALID DATA_INVALID ENCRYPTION_DECLINED MSG messages.sendEncryptedFile,user,MSG_WAIT_FAILED messages.sendEncryptedService,user,DATA_INVALID ENCRYPTION_DECLINED MSG_WAIT_FAILED USER_IS_BLOCKED messages.sendInlineBotResult,user,CHAT_SEND_INLINE_FORBIDDEN CHAT_WRITE_FORBIDDEN INLINE_RESULT_EXPIRED PEER_ID_INVALID QUERY_ID_EMPTY SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH WEBPAGE_CURL_FAILED WEBPAGE_MEDIA_EMPTY -messages.sendMedia,both,BOT_PAYMENTS_DISABLED BOT_POLLS_DISABLED BROADCAST_PUBLIC_VOTERS_FORBIDDEN CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_SEND_MEDIA_FORBIDDEN CHAT_WRITE_FORBIDDEN EMOTICON_INVALID EXTERNAL_URL_INVALID FILE_PARTS_INVALID FILE_PART_LENGTH_INVALID FILE_REFERENCE_EMPTY FILE_REFERENCE_EXPIRED GAME_BOT_INVALID INPUT_USER_DEACTIVATED MEDIA_CAPTION_TOO_LONG MEDIA_EMPTY PAYMENT_PROVIDER_INVALID PEER_ID_INVALID PHOTO_EXT_INVALID PHOTO_INVALID_DIMENSIONS PHOTO_SAVE_FILE_INVALID POLL_ANSWERS_INVALID POLL_OPTION_DUPLICATE POLL_QUESTION_INVALID QUIZ_CORRECT_ANSWERS_EMPTY QUIZ_CORRECT_ANSWERS_TOO_MUCH QUIZ_CORRECT_ANSWER_INVALID QUIZ_MULTIPLE_INVALID RANDOM_ID_DUPLICATE SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH STORAGE_CHECK_FAILED TIMEOUT USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT VIDEO_CONTENT_TYPE_INVALID WEBPAGE_CURL_FAILED WEBPAGE_MEDIA_EMPTY +messages.sendMedia,both,BOT_PAYMENTS_DISABLED BOT_POLLS_DISABLED BROADCAST_PUBLIC_VOTERS_FORBIDDEN CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_SEND_MEDIA_FORBIDDEN CHAT_WRITE_FORBIDDEN CURRENCY_TOTAL_AMOUNT_INVALID EMOTICON_INVALID EXTERNAL_URL_INVALID FILE_PARTS_INVALID FILE_PART_LENGTH_INVALID FILE_REFERENCE_EMPTY FILE_REFERENCE_EXPIRED GAME_BOT_INVALID INPUT_USER_DEACTIVATED MEDIA_CAPTION_TOO_LONG MEDIA_EMPTY PAYMENT_PROVIDER_INVALID PEER_ID_INVALID PHOTO_EXT_INVALID PHOTO_INVALID_DIMENSIONS PHOTO_SAVE_FILE_INVALID POLL_ANSWERS_INVALID POLL_OPTION_DUPLICATE POLL_QUESTION_INVALID QUIZ_CORRECT_ANSWERS_EMPTY QUIZ_CORRECT_ANSWERS_TOO_MUCH QUIZ_CORRECT_ANSWER_INVALID QUIZ_MULTIPLE_INVALID RANDOM_ID_DUPLICATE SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH STORAGE_CHECK_FAILED TIMEOUT USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT VIDEO_CONTENT_TYPE_INVALID WEBPAGE_CURL_FAILED WEBPAGE_MEDIA_EMPTY messages.sendMessage,both,AUTH_KEY_DUPLICATED BOT_DOMAIN_INVALID BUTTON_DATA_INVALID BUTTON_TYPE_INVALID BUTTON_URL_INVALID CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_ID_INVALID CHAT_RESTRICTED CHAT_WRITE_FORBIDDEN ENTITIES_TOO_LONG ENTITY_MENTION_USER_INVALID INPUT_USER_DEACTIVATED MESSAGE_EMPTY MESSAGE_TOO_LONG MSG_ID_INVALID PEER_ID_INVALID POLL_OPTION_INVALID RANDOM_ID_DUPLICATE REPLY_MARKUP_INVALID REPLY_MARKUP_TOO_LONG SCHEDULE_BOT_NOT_ALLOWED SCHEDULE_DATE_TOO_LATE SCHEDULE_STATUS_PRIVATE SCHEDULE_TOO_MUCH TIMEOUT USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT YOU_BLOCKED_USER messages.sendMultiMedia,both,MULTI_MEDIA_TOO_LONG SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH messages.sendScheduledMessages,user, @@ -286,7 +286,7 @@ messages.setBotShippingResults,both,QUERY_ID_INVALID messages.setEncryptedTyping,user,CHAT_ID_INVALID messages.setGameScore,bot,PEER_ID_INVALID USER_BOT_REQUIRED messages.setHistoryTTL,user,CHAT_NOT_MODIFIED TTL_PERIOD_INVALID -messages.setInlineBotResults,bot,ARTICLE_TITLE_EMPTY AUDIO_CONTENT_URL_EMPTY AUDIO_TITLE_EMPTY BUTTON_DATA_INVALID BUTTON_TYPE_INVALID BUTTON_URL_INVALID DOCUMENT_INVALID GIF_CONTENT_TYPE_INVALID MESSAGE_EMPTY NEXT_OFFSET_INVALID PHOTO_CONTENT_TYPE_INVALID PHOTO_CONTENT_URL_EMPTY PHOTO_THUMB_URL_EMPTY QUERY_ID_INVALID REPLY_MARKUP_INVALID RESULT_TYPE_INVALID SEND_MESSAGE_MEDIA_INVALID SEND_MESSAGE_TYPE_INVALID START_PARAM_INVALID STICKER_DOCUMENT_INVALID USER_BOT_INVALID VIDEO_TITLE_EMPTY WEBDOCUMENT_MIME_INVALID WEBDOCUMENT_URL_INVALID +messages.setInlineBotResults,bot,ARTICLE_TITLE_EMPTY AUDIO_CONTENT_URL_EMPTY AUDIO_TITLE_EMPTY BUTTON_DATA_INVALID BUTTON_TYPE_INVALID BUTTON_URL_INVALID DOCUMENT_INVALID FILE_CONTENT_TYPE_INVALID GIF_CONTENT_TYPE_INVALID MESSAGE_EMPTY NEXT_OFFSET_INVALID PHOTO_CONTENT_TYPE_INVALID PHOTO_CONTENT_URL_EMPTY PHOTO_THUMB_URL_EMPTY QUERY_ID_INVALID REPLY_MARKUP_INVALID RESULT_TYPE_INVALID SEND_MESSAGE_MEDIA_INVALID SEND_MESSAGE_TYPE_INVALID START_PARAM_INVALID STICKER_DOCUMENT_INVALID USER_BOT_INVALID VIDEO_TITLE_EMPTY WEBDOCUMENT_MIME_INVALID WEBDOCUMENT_URL_INVALID messages.setInlineGameScore,bot,MESSAGE_ID_INVALID USER_BOT_REQUIRED messages.setTyping,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ID_INVALID CHAT_WRITE_FORBIDDEN PEER_ID_INVALID USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT messages.startBot,user,BOT_INVALID PEER_ID_INVALID START_PARAM_EMPTY START_PARAM_INVALID @@ -310,6 +310,7 @@ phone.acceptCall,user,CALL_ALREADY_ACCEPTED CALL_ALREADY_DECLINED CALL_OCCUPY_FA phone.confirmCall,user,CALL_ALREADY_DECLINED CALL_PEER_INVALID phone.createGroupCall,user,SCHEDULE_DATE_INVALID phone.discardCall,user,CALL_ALREADY_ACCEPTED CALL_PEER_INVALID +phone.discardGroupCallRequest,user,GROUPCALL_ALREADY_DISCARDED phone.editGroupCallParticipant,user,USER_VOLUME_INVALID phone.getCallConfig,user, phone.inviteToGroupCall,user,GROUPCALL_FORBIDDEN From 2df1dd7215362a3d6721a7aba64ebceb3061ed5c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 9 Jul 2021 20:08:04 +0200 Subject: [PATCH 004/256] Don't call getFullChannel during iter_participants unless necessary This should reduce the floodwaits of this request by a lot. --- telethon/client/chats.py | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/telethon/client/chats.py b/telethon/client/chats.py index d712f698..ca4e771b 100644 --- a/telethon/client/chats.py +++ b/telethon/client/chats.py @@ -124,11 +124,11 @@ class _ParticipantsIter(RequestIter): self.requests = [] if ty == helpers._EntityType.CHANNEL: - self.total = (await self.client( - functions.channels.GetFullChannelRequest(entity) - )).full_chat.participants_count - if self.limit <= 0: + # May not have access to the channel, but getFull can get the .total. + self.total = (await self.client( + functions.channels.GetFullChannelRequest(entity) + )).full_chat.participants_count raise StopAsyncIteration self.seen = set() @@ -201,9 +201,29 @@ class _ParticipantsIter(RequestIter): if self.requests[0].offset > self.limit: return True + if self.total is None: + f = self.requests[0].filter + if len(self.requests) > 1 or ( + not isinstance(f, types.ChannelParticipantsRecent) + and (not isinstance(f, types.ChannelParticipantsSearch) or f.q) + ): + # Only do an additional getParticipants here to get the total + # if there's a filter which would reduce the real total number. + # getParticipants is cheaper than getFull. + self.total = (await self.client(functions.channels.GetParticipantsRequest( + channel=self.requests[0].channel, + filter=types.ChannelParticipantsRecent(), + offset=0, + limit=1, + hash=0 + ))).count + results = await self.client(self.requests) for i in reversed(range(len(self.requests))): participants = results[i] + if self.total is None: + # Will only get here if there was one request with a filter that matched all users. + self.total = participants.count if not participants.users: self.requests.pop(i) continue From 06afd04b07b158354ece47f476983273e80bfa72 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 9 Jul 2021 20:18:22 +0200 Subject: [PATCH 005/256] Update to version 1.23 --- readthedocs/misc/changelog.rst | 23 +++++++++++++++++++++++ telethon/version.py | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/readthedocs/misc/changelog.rst b/readthedocs/misc/changelog.rst index 564effdd..951cf2e0 100644 --- a/readthedocs/misc/changelog.rst +++ b/readthedocs/misc/changelog.rst @@ -13,6 +13,29 @@ it can take advantage of new goodies! .. contents:: List of All Versions +New schema and bug fixes (v1.23) +================================ + ++------------------------+ +| Scheme layer used: 130 | ++------------------------+ + +`View new and changed raw API methods `__. + +Enhancements +~~~~~~~~~~~~ + +* `client.pin_message() ` + can now pin on a single side in PMs. +* Iterating participants should now be less expensive floodwait-wise. + +Bug fixes +~~~~~~~~~ + +* The QR login URL was being encoded incorrectly. +* ``force_document`` was being ignored in inline queries for document. +* ``manage_call`` permission was accidentally set to ``True`` by default. + New schema and bug fixes (v1.22) ================================ diff --git a/telethon/version.py b/telethon/version.py index 1b916db8..ab80aa1b 100644 --- a/telethon/version.py +++ b/telethon/version.py @@ -1,3 +1,3 @@ # Versions should comply with PEP440. # This line is parsed in setup.py: -__version__ = '1.22.0' +__version__ = '1.23.0' From 3570953d14a9e72ddcb84154071648291f81feb4 Mon Sep 17 00:00:00 2001 From: Shrimadhav U K Date: Sat, 17 Jul 2021 00:31:08 +0530 Subject: [PATCH 006/256] Update to layer 131 (#3112) --- telethon_generator/data/api.tl | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/telethon_generator/data/api.tl b/telethon_generator/data/api.tl index 8b070368..b001ef20 100644 --- a/telethon_generator/data/api.tl +++ b/telethon_generator/data/api.tl @@ -376,6 +376,7 @@ updateChatParticipant#f3b3781f flags:# chat_id:int date:int actor_id:int user_id updateChannelParticipant#7fecb1ec flags:# channel_id:int date:int actor_id:int user_id:int prev_participant:flags.0?ChannelParticipant new_participant:flags.1?ChannelParticipant invite:flags.2?ExportedChatInvite qts:int = Update; updateBotStopped#7f9488a user_id:int date:int stopped:Bool qts:int = Update; updateGroupCallConnection#b783982 flags:# presentation:flags.0?true params:DataJSON = Update; +updateBotCommands#cf7e0873 peer:Peer bot_id:int commands:Vector = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -534,7 +535,7 @@ authorization#ad01d61d flags:# current:flags.0?true official_app:flags.1?true pa account.authorizations#1250abde authorizations:Vector = account.Authorizations; -account.password#ad2641f8 flags:# has_recovery:flags.0?true has_secure_values:flags.1?true has_password:flags.2?true current_algo:flags.2?PasswordKdfAlgo srp_B:flags.2?bytes srp_id:flags.2?long hint:flags.3?string email_unconfirmed_pattern:flags.4?string new_algo:PasswordKdfAlgo new_secure_algo:SecurePasswordKdfAlgo secure_random:bytes = account.Password; +account.password#185b184f flags:# has_recovery:flags.0?true has_secure_values:flags.1?true has_password:flags.2?true current_algo:flags.2?PasswordKdfAlgo srp_B:flags.2?bytes srp_id:flags.2?long hint:flags.3?string email_unconfirmed_pattern:flags.4?string new_algo:PasswordKdfAlgo new_secure_algo:SecurePasswordKdfAlgo secure_random:bytes pending_reset_date:flags.5?int = account.Password; account.passwordSettings#9a5c33e5 flags:# email:flags.0?string secure_settings:flags.1?SecureSecretSettings = account.PasswordSettings; @@ -1205,7 +1206,7 @@ peerBlocked#e8fd8014 peer_id:Peer date:int = PeerBlocked; stats.messageStats#8999f295 views_graph:StatsGraph = stats.MessageStats; groupCallDiscarded#7780bcb4 id:long access_hash:long duration:int = GroupCall; -groupCall#653dbaad flags:# join_muted:flags.1?true can_change_join_muted:flags.2?true join_date_asc:flags.6?true schedule_start_subscribed:flags.8?true can_start_video:flags.9?true id:long access_hash:long participants_count:int title:flags.3?string stream_dc_id:flags.4?int record_start_date:flags.5?int schedule_date:flags.7?int version:int = GroupCall; +groupCall#d597650c flags:# join_muted:flags.1?true can_change_join_muted:flags.2?true join_date_asc:flags.6?true schedule_start_subscribed:flags.8?true can_start_video:flags.9?true id:long access_hash:long participants_count:int title:flags.3?string stream_dc_id:flags.4?int record_start_date:flags.5?int schedule_date:flags.7?int unmuted_video_count:flags.10?int unmuted_video_limit:int version:int = GroupCall; inputGroupCall#d8aa840f id:long access_hash:long = InputGroupCall; @@ -1248,7 +1249,7 @@ phone.exportedGroupCallInvite#204bd158 link:string = phone.ExportedGroupCallInvi groupCallParticipantVideoSourceGroup#dcb118b7 semantics:string sources:Vector = GroupCallParticipantVideoSourceGroup; -groupCallParticipantVideo#78e41663 flags:# paused:flags.0?true endpoint:string source_groups:Vector = GroupCallParticipantVideo; +groupCallParticipantVideo#67753ac8 flags:# paused:flags.0?true endpoint:string source_groups:Vector audio_source:flags.1?int = GroupCallParticipantVideo; stickers.suggestedShortName#85fea03f short_name:string = stickers.SuggestedShortName; @@ -1260,6 +1261,10 @@ botCommandScopePeer#db9d897d peer:InputPeer = BotCommandScope; botCommandScopePeerAdmins#3fd863d1 peer:InputPeer = BotCommandScope; botCommandScopePeerUser#a1321f3 peer:InputPeer user_id:InputUser = BotCommandScope; +account.resetPasswordFailedWait#e3779861 retry_date:int = account.ResetPasswordResult; +account.resetPasswordRequestedWait#e9effc7d until_date:int = account.ResetPasswordResult; +account.resetPasswordOk#e926d63e = account.ResetPasswordResult; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1281,13 +1286,14 @@ auth.bindTempAuthKey#cdd42a05 perm_auth_key_id:long nonce:long expires_at:int en auth.importBotAuthorization#67a3ff2c flags:int api_id:int api_hash:string bot_auth_token:string = auth.Authorization; auth.checkPassword#d18b4d16 password:InputCheckPasswordSRP = auth.Authorization; auth.requestPasswordRecovery#d897bc66 = auth.PasswordRecovery; -auth.recoverPassword#4ea56e92 code:string = auth.Authorization; +auth.recoverPassword#37096c70 flags:# code:string new_settings:flags.0?account.PasswordInputSettings = auth.Authorization; auth.resendCode#3ef1a9bf phone_number:string phone_code_hash:string = auth.SentCode; auth.cancelCode#1f040578 phone_number:string phone_code_hash:string = Bool; auth.dropTempAuthKeys#8e48a188 except_auth_keys:Vector = Bool; auth.exportLoginToken#b1b41517 api_id:int api_hash:string except_ids:Vector = auth.LoginToken; auth.importLoginToken#95ac5ce4 token:bytes = auth.LoginToken; auth.acceptLoginToken#e894ad4d token:bytes = Authorization; +auth.checkRecoveryPassword#d36bf79 code:string = Bool; account.registerDevice#68976c6f flags:# no_muted:flags.0?true token_type:int token:string app_sandbox:Bool secret:bytes other_uids:Vector = Bool; account.unregisterDevice#3076c4bf token_type:int token:string other_uids:Vector = Bool; @@ -1357,6 +1363,8 @@ account.getMultiWallPapers#65ad71dc wallpapers:Vector = Vector = Vector; users.getFullUser#ca30a5b1 id:InputUser = UserFull; @@ -1670,4 +1678,4 @@ stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel stats.getMessagePublicForwards#5630281b channel:InputChannel msg_id:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats; -// LAYER 130 +// LAYER 131 From 79866750d257756de4247b4f3e38a27ec14f1d3b Mon Sep 17 00:00:00 2001 From: Devesh Pal <69723581+New-dev0@users.noreply.github.com> Date: Wed, 21 Jul 2021 02:34:09 +0530 Subject: [PATCH 007/256] Add new known RPCErrors (#3114) --- telethon_generator/data/errors.csv | 4 ++++ telethon_generator/data/methods.csv | 8 +++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/telethon_generator/data/errors.csv b/telethon_generator/data/errors.csv index 26d4c774..498006b9 100644 --- a/telethon_generator/data/errors.csv +++ b/telethon_generator/data/errors.csv @@ -175,6 +175,7 @@ INTERDC_X_CALL_RICH_ERROR,500,A rich error occurred while communicating with DC INVITE_HASH_EMPTY,400,The invite hash is empty INVITE_HASH_EXPIRED,400,The chat the user tried to join has expired and is not valid anymore INVITE_HASH_INVALID,400,The invite hash is invalid +LANG_CODE_INVALID,400, LANG_PACK_INVALID,400,The provided language pack is invalid LASTNAME_INVALID,400,The last name is invalid LIMIT_INVALID,400,An invalid limit was provided. See https://core.telegram.org/api/files#downloading-files @@ -229,6 +230,7 @@ PARTICIPANT_VERSION_OUTDATED,400,The other participant does not use an up to dat PASSWORD_EMPTY,400,The provided password is empty PASSWORD_HASH_INVALID,400,The password (and thus its hash value) you entered is invalid PASSWORD_MISSING,400,The account must have 2-factor authentication enabled (a password) before this method can be used +PASSWORD_RECOVERY_EXPIRED,400, PASSWORD_REQUIRED,400,The account must have 2-factor authentication enabled (a password) before this method can be used PASSWORD_TOO_FRESH_X,400,The password was added too recently and {seconds} seconds must pass before using the method PAYMENT_PROVIDER_INVALID,400,The payment provider was not recognised or its token was invalid @@ -289,6 +291,7 @@ REG_ID_GENERATE_FAILED,500,Failure while generating registration ID REPLY_MARKUP_GAME_EMPTY,400,The provided reply markup for the game is empty REPLY_MARKUP_INVALID,400,The provided reply markup is invalid REPLY_MARKUP_TOO_LONG,400,The data embedded in the reply markup buttons was too much +RESET_REQUEST_MISSING,400, RESULTS_TOO_MUCH,400,"You sent too many results, see https://core.telegram.org/bots/api#answerinlinequery for the current limit" RESULT_ID_DUPLICATE,400,Duplicated IDs on the sent results. Make sure to use unique IDs RESULT_ID_INVALID,400,The given result cannot be used to send the selection to the bot @@ -313,6 +316,7 @@ SESSION_REVOKED,401,"The authorization has been invalidated, because of the user SESSION_TOO_FRESH_X,400,The session logged in too recently and {seconds} seconds must pass before calling the method SHA256_HASH_INVALID,400,The provided SHA256 hash is invalid SHORTNAME_OCCUPY_FAILED,400,An error occurred when trying to register the short-name used for the sticker pack. Try a different name +SHORT_NAME_INVALID,400, SHORT_NAME_OCCUPIED,400, SLOWMODE_WAIT_X,420,A wait of {seconds} seconds is required before sending another message in this chat SRP_ID_INVALID,400, diff --git a/telethon_generator/data/methods.csv b/telethon_generator/data/methods.csv index 72900127..9e92a452 100644 --- a/telethon_generator/data/methods.csv +++ b/telethon_generator/data/methods.csv @@ -6,6 +6,7 @@ account.checkUsername,user,USERNAME_INVALID account.confirmPasswordEmail,user, account.confirmPhone,user,CODE_HASH_INVALID PHONE_CODE_EMPTY account.createTheme,user,THEME_MIME_INVALID +account.declinePasswordReset,user,RESET_REQUEST_MISSING account.deleteAccount,user,2FA_CONFIRM_WAIT_X account.deleteSecureValue,user, account.finishTakeoutSession,user, @@ -69,6 +70,7 @@ auth.acceptLoginToken,user, auth.bindTempAuthKey,both,ENCRYPTED_MESSAGE_INVALID INPUT_REQUEST_TOO_LONG TEMP_AUTH_KEY_EMPTY TIMEOUT auth.cancelCode,user,PHONE_NUMBER_INVALID auth.checkPassword,user,PASSWORD_HASH_INVALID +auth.checkRecoveryPassword,user,PASSWORD_RECOVERY_EXPIRED auth.dropTempAuthKeys,both, auth.exportAuthorization,both,DC_ID_INVALID auth.exportLoginToken,user, @@ -76,7 +78,7 @@ auth.importAuthorization,both,AUTH_BYTES_INVALID USER_ID_INVALID auth.importBotAuthorization,both,ACCESS_TOKEN_EXPIRED ACCESS_TOKEN_INVALID API_ID_INVALID auth.importLoginToken,user,AUTH_TOKEN_ALREADY_ACCEPTED AUTH_TOKEN_EXPIRED AUTH_TOKEN_INVALID auth.logOut,both, -auth.recoverPassword,user,CODE_EMPTY +auth.recoverPassword,user,CODE_EMPTY NEW_SETTINGS_INVALID auth.requestPasswordRecovery,user,PASSWORD_EMPTY auth.resendCode,user,PHONE_NUMBER_INVALID auth.resetAuthorizations,user,TIMEOUT @@ -85,7 +87,7 @@ auth.signIn,user,PHONE_CODE_EMPTY PHONE_CODE_EXPIRED PHONE_CODE_INVALID PHONE_NU auth.signUp,user,FIRSTNAME_INVALID MEMBER_OCCUPY_PRIMARY_LOC_FAILED PHONE_CODE_EMPTY PHONE_CODE_EXPIRED PHONE_CODE_INVALID PHONE_NUMBER_FLOOD PHONE_NUMBER_INVALID PHONE_NUMBER_OCCUPIED REG_ID_GENERATE_FAILED bots.answerWebhookJSONQuery,bot,QUERY_ID_INVALID USER_BOT_INVALID bots.sendCustomRequest,bot,USER_BOT_INVALID -bots.setBotCommands,bot,BOT_COMMAND_DESCRIPTION_INVALID BOT_COMMAND_INVALID +bots.setBotCommands,bot,BOT_COMMAND_DESCRIPTION_INVALID BOT_COMMAND_INVALID LANG_CODE_INVALID channels.checkUsername,user,CHANNEL_INVALID CHAT_ID_INVALID USERNAME_INVALID channels.createChannel,user,CHAT_TITLE_EMPTY USER_RESTRICTED channels.deleteChannel,user,CHANNEL_INVALID CHANNEL_PRIVATE @@ -336,7 +338,7 @@ stats.getMegagroupStats,user,CHAT_ADMIN_REQUIRED MEGAGROUP_REQUIRED STATS_MIGRAT stats.loadAsyncGraph,user,GRAPH_INVALID_RELOAD GRAPH_OUTDATED_RELOAD stickers.addStickerToSet,bot,BOT_MISSING STICKERSET_INVALID STICKER_PNG_NOPNG STICKER_TGS_NOTGS stickers.changeStickerPosition,bot,BOT_MISSING STICKER_INVALID -stickers.checkShortName,both,SHORT_NAME_OCCUPIED +stickers.checkShortName,both,SHORT_NAME_INVALID SHORT_NAME_OCCUPIED stickers.createStickerSet,bot,BOT_MISSING PACK_SHORT_NAME_INVALID PACK_SHORT_NAME_OCCUPIED PEER_ID_INVALID SHORTNAME_OCCUPY_FAILED STICKERS_EMPTY STICKER_EMOJI_INVALID STICKER_FILE_INVALID STICKER_PNG_DIMENSIONS STICKER_PNG_NOPNG STICKER_TGS_NOTGS STICKER_THUMB_PNG_NOPNG STICKER_THUMB_TGS_NOTGS USER_ID_INVALID stickers.removeStickerFromSet,bot,BOT_MISSING STICKER_INVALID stickers.setStickerSetThumb,bot,STICKER_THUMB_PNG_NOPNG STICKER_THUMB_TGS_NOTGS From e2d97b44c59a05980c530604cfd1ca7e370ebdc0 Mon Sep 17 00:00:00 2001 From: Joshua Coales Date: Tue, 3 Aug 2021 17:33:17 +0100 Subject: [PATCH 008/256] Update docs to have fewer grammatical mistakes (#3121) --- readthedocs/concepts/botapi-vs-mtproto.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readthedocs/concepts/botapi-vs-mtproto.rst b/readthedocs/concepts/botapi-vs-mtproto.rst index cddb52d0..224bca08 100644 --- a/readthedocs/concepts/botapi-vs-mtproto.rst +++ b/readthedocs/concepts/botapi-vs-mtproto.rst @@ -145,7 +145,7 @@ After using Telethon: Key differences: -* The recommended way to do it imports less things. +* The recommended way to do it imports fewer things. * All handlers trigger by default, so we need ``events.StopPropagation``. * Adding handlers, responding and running is a lot less verbose. * Telethon needs ``async def`` and ``await``. From 196cef66fd55184cab9704e2687a401db3cc2d7d Mon Sep 17 00:00:00 2001 From: alexkoay Date: Wed, 4 Aug 2021 00:33:46 +0800 Subject: [PATCH 009/256] Fix typehint for callback in UpdateMethods (#3119) --- telethon/client/updates.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/telethon/client/updates.py b/telethon/client/updates.py index a9d6344e..5bdf5893 100644 --- a/telethon/client/updates.py +++ b/telethon/client/updates.py @@ -16,6 +16,8 @@ if typing.TYPE_CHECKING: from .telegramclient import TelegramClient +Callback = typing.Callable[[typing.Any], typing.Any] + class UpdateMethods: # region Public methods @@ -104,7 +106,7 @@ class UpdateMethods: def add_event_handler( self: 'TelegramClient', - callback: callable, + callback: Callback, event: EventBuilder = None): """ Registers a new event handler callback. @@ -153,7 +155,7 @@ class UpdateMethods: def remove_event_handler( self: 'TelegramClient', - callback: callable, + callback: Callback, event: EventBuilder = None) -> int: """ Inverse operation of `add_event_handler()`. @@ -191,7 +193,7 @@ class UpdateMethods: return found def list_event_handlers(self: 'TelegramClient')\ - -> 'typing.Sequence[typing.Tuple[callable, EventBuilder]]': + -> 'typing.Sequence[typing.Tuple[Callback, EventBuilder]]': """ Lists all registered event handlers. From ad55b945c1dff0dbeab226b00301cb4ef634d5bb Mon Sep 17 00:00:00 2001 From: Devesh Pal Date: Tue, 3 Aug 2021 22:04:10 +0530 Subject: [PATCH 010/256] Add comment_to to InlineResult-click method (#3118) --- telethon/tl/custom/inlineresult.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/telethon/tl/custom/inlineresult.py b/telethon/tl/custom/inlineresult.py index f189068c..97493281 100644 --- a/telethon/tl/custom/inlineresult.py +++ b/telethon/tl/custom/inlineresult.py @@ -102,7 +102,7 @@ class InlineResult: elif isinstance(self.result, types.BotInlineMediaResult): return self.result.document - async def click(self, entity=None, reply_to=None, + async def click(self, entity=None, reply_to=None, comment_to=None, silent=False, clear_draft=False, hide_via=False): """ Clicks this result and sends the associated `message`. @@ -114,6 +114,11 @@ class InlineResult: reply_to (`int` | `Message `, optional): If present, the sent message will reply to this ID or message. + comment_to (`int` | `Message `, optional): + Similar to ``reply_to``, but replies in the linked group of a + broadcast channel instead (effectively leaving a "comment to" + the specified message). + silent (`bool`, optional): Whether the message should notify people with sound or not. Defaults to `False` (send with a notification sound unless @@ -135,7 +140,11 @@ class InlineResult: else: raise ValueError('You must provide the entity where the result should be sent to') - reply_id = None if reply_to is None else utils.get_message_id(reply_to) + if comment_to: + entity, reply_id = await self._client._get_comment_data(entity, comment_to) + else: + reply_id = None if reply_to is None else utils.get_message_id(reply_to) + req = functions.messages.SendInlineBotResultRequest( peer=entity, query_id=self._query_id, From e5599c178b9789df503ac90b1badd42aa8ed8681 Mon Sep 17 00:00:00 2001 From: Devesh Pal Date: Thu, 5 Aug 2021 14:24:07 +0530 Subject: [PATCH 011/256] Expose more raw API params in friendly methods (#3104) --- telethon/client/messages.py | 19 ++++++++++++++++++- telethon/client/uploads.py | 16 +++++++++++----- telethon/tl/custom/button.py | 20 ++++++++++++++++---- telethon/tl/custom/inlineresult.py | 10 ++++++++-- 4 files changed, 53 insertions(+), 12 deletions(-) diff --git a/telethon/client/messages.py b/telethon/client/messages.py index 31a954f1..1cb829c0 100644 --- a/telethon/client/messages.py +++ b/telethon/client/messages.py @@ -608,6 +608,7 @@ class MessageMethods: clear_draft: bool = False, buttons: 'hints.MarkupLike' = None, silent: bool = None, + background: bool = None, supports_streaming: bool = False, schedule: 'hints.DateLike' = None, comment_to: 'typing.Union[int, types.Message]' = None @@ -701,6 +702,9 @@ class MessageMethods: channel or not. Defaults to `False`, which means it will notify them. Set it to `True` to alter this behaviour. + background (`bool`, optional): + Whether the message should be send in background. + supports_streaming (`bool`, optional): Whether the sent video supports streaming or not. Note that Telegram only recognizes as streamable some formats like MP4, @@ -788,7 +792,7 @@ class MessageMethods: buttons=buttons, clear_draft=clear_draft, silent=silent, schedule=schedule, supports_streaming=supports_streaming, formatting_entities=formatting_entities, - comment_to=comment_to + comment_to=comment_to, background=background ) entity = await self.get_input_entity(entity) @@ -811,6 +815,7 @@ class MessageMethods: message.media, caption=message.message, silent=silent, + background=background, reply_to=reply_to, buttons=markup, formatting_entities=message.entities, @@ -821,6 +826,7 @@ class MessageMethods: peer=entity, message=message.message or '', silent=silent, + background=background, reply_to_msg_id=utils.get_message_id(reply_to), reply_markup=markup, entities=message.entities, @@ -846,6 +852,7 @@ class MessageMethods: reply_to_msg_id=utils.get_message_id(reply_to), clear_draft=clear_draft, silent=silent, + background=background, reply_markup=self.build_reply_markup(buttons), schedule_date=schedule ) @@ -874,6 +881,8 @@ class MessageMethods: messages: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]', from_peer: 'hints.EntityLike' = None, *, + background: bool = None, + with_my_score: bool = None, silent: bool = None, as_album: bool = None, schedule: 'hints.DateLike' = None @@ -906,6 +915,12 @@ class MessageMethods: the person has the chat muted). Set it to `True` to alter this behaviour. + background (`bool`, optional): + Whether the message should be forwarded in background. + + with_my_score (`bool`, optional): + Whether forwarded should contain your game score. + as_album (`bool`, optional): This flag no longer has any effect. @@ -980,6 +995,8 @@ class MessageMethods: id=chunk, to_peer=entity, silent=silent, + background=background, + with_my_score=with_my_score, schedule_date=schedule ) result = await self(req) diff --git a/telethon/client/uploads.py b/telethon/client/uploads.py index 544b7087..c2e08fde 100644 --- a/telethon/client/uploads.py +++ b/telethon/client/uploads.py @@ -112,6 +112,7 @@ class UploadMethods: video_note: bool = False, buttons: 'hints.MarkupLike' = None, silent: bool = None, + background: bool = None, supports_streaming: bool = False, schedule: 'hints.DateLike' = None, comment_to: 'typing.Union[int, types.Message]' = None, @@ -249,6 +250,9 @@ class UploadMethods: the person has the chat muted). Set it to `True` to alter this behaviour. + background (`bool`, optional): + Whether the message should be send in background. + supports_streaming (`bool`, optional): Whether the sent video supports streaming or not. Note that Telegram only recognizes as streamable some formats like MP4, @@ -347,7 +351,7 @@ class UploadMethods: progress_callback=progress_callback, reply_to=reply_to, parse_mode=parse_mode, silent=silent, schedule=schedule, supports_streaming=supports_streaming, clear_draft=clear_draft, - force_document=force_document + force_document=force_document, background=background, ) file = file[10:] captions = captions[10:] @@ -360,7 +364,7 @@ class UploadMethods: attributes=attributes, thumb=thumb, voice_note=voice_note, video_note=video_note, buttons=buttons, silent=silent, supports_streaming=supports_streaming, schedule=schedule, - clear_draft=clear_draft, + clear_draft=clear_draft, background=background, **kwargs )) @@ -389,7 +393,8 @@ class UploadMethods: request = functions.messages.SendMediaRequest( entity, media, reply_to_msg_id=reply_to, message=caption, entities=msg_entities, reply_markup=markup, silent=silent, - schedule_date=schedule, clear_draft=clear_draft + schedule_date=schedule, clear_draft=clear_draft, + background=background ) return self._get_response_message(request, await self(request), entity) @@ -397,7 +402,7 @@ class UploadMethods: progress_callback=None, reply_to=None, parse_mode=(), silent=None, schedule=None, supports_streaming=None, clear_draft=None, - force_document=False): + force_document=False, background=None): """Specialized version of .send_file for albums""" # We don't care if the user wants to avoid cache, we will use it # anyway. Why? The cached version will be exactly the same thing @@ -456,7 +461,8 @@ class UploadMethods: # Now we can construct the multi-media request request = functions.messages.SendMultiMediaRequest( entity, reply_to_msg_id=reply_to, multi_media=media, - silent=silent, schedule_date=schedule, clear_draft=clear_draft + silent=silent, schedule_date=schedule, clear_draft=clear_draft, + background=background ) result = await self(request) diff --git a/telethon/tl/custom/button.py b/telethon/tl/custom/button.py index 134fbec7..e8003992 100644 --- a/telethon/tl/custom/button.py +++ b/telethon/tl/custom/button.py @@ -245,17 +245,29 @@ class Button: resize=resize, single_use=single_use, selective=selective) @staticmethod - def clear(): + def clear(selective=None): """ Clears all keyboard buttons after sending a message with this markup. When used, no other button should be present or it will be ignored. + + ``selective`` is as documented in `text`. + """ - return types.ReplyKeyboardHide() + return types.ReplyKeyboardHide(selective=selective) @staticmethod - def force_reply(): + def force_reply(single_use=None, selective=None, placeholder=None): """ Forces a reply to the message with this markup. If used, no other button should be present or it will be ignored. + + ``single_use`` and ``selective`` are as documented in `text`. + + Args: + placeholder (str): + text to show the user at typing place of message. """ - return types.ReplyKeyboardForceReply() + return types.ReplyKeyboardForceReply( + single_use=single_use, + selective=selective, + placeholder=placeholder) diff --git a/telethon/tl/custom/inlineresult.py b/telethon/tl/custom/inlineresult.py index 97493281..15639aa5 100644 --- a/telethon/tl/custom/inlineresult.py +++ b/telethon/tl/custom/inlineresult.py @@ -103,7 +103,8 @@ class InlineResult: return self.result.document async def click(self, entity=None, reply_to=None, comment_to=None, - silent=False, clear_draft=False, hide_via=False): + silent=False, clear_draft=False, hide_via=False, + background=None): """ Clicks this result and sends the associated `message`. @@ -128,10 +129,14 @@ class InlineResult: clear_draft (`bool`, optional): Whether the draft should be removed after sending the message from this result or not. Defaults to `False`. - + hide_via (`bool`, optional): Whether the "via @bot" should be hidden or not. Only works with certain bots (like @bing or @gif). + + background (`bool`, optional): + Whether the message should be send in background. + """ if entity: entity = await self._client.get_input_entity(entity) @@ -150,6 +155,7 @@ class InlineResult: query_id=self._query_id, id=self.result.id, silent=silent, + background=background, clear_draft=clear_draft, hide_via=hide_via, reply_to_msg_id=reply_id From e546ae2f85632695e771c109c7cabb15350fbb8e Mon Sep 17 00:00:00 2001 From: penn5 Date: Fri, 6 Aug 2021 07:13:34 +0100 Subject: [PATCH 012/256] Allow per-request flood sleep threshold selection (#3123) --- telethon/client/telegrambaseclient.py | 5 +++++ telethon/client/users.py | 8 +++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/telethon/client/telegrambaseclient.py b/telethon/client/telegrambaseclient.py index ee40c796..e8192d73 100644 --- a/telethon/client/telegrambaseclient.py +++ b/telethon/client/telegrambaseclient.py @@ -837,6 +837,11 @@ class TelegramBaseClient(abc.ABC): executed sequentially on the server. They run in arbitrary order by default. + flood_sleep_threshold (`int` | `None`, optional): + The flood sleep threshold to use for this request. This overrides + the default value stored in + `client.flood_sleep_threshold ` + Returns: The result of the request (often a `TLObject`) or a list of results if more than one request was given. diff --git a/telethon/client/users.py b/telethon/client/users.py index 646914b2..49408896 100644 --- a/telethon/client/users.py +++ b/telethon/client/users.py @@ -26,10 +26,12 @@ def _fmt_flood(delay, request, *, early=False, td=datetime.timedelta): class UserMethods: - async def __call__(self: 'TelegramClient', request, ordered=False): + async def __call__(self: 'TelegramClient', request, ordered=False, flood_sleep_threshold=None): return await self._call(self._sender, request, ordered=ordered) - async def _call(self: 'TelegramClient', sender, request, ordered=False): + async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sleep_threshold=None): + if flood_sleep_threshold is None: + flood_sleep_threshold = self.flood_sleep_threshold requests = (request if utils.is_list_like(request) else (request,)) for r in requests: if not isinstance(r, TLRequest): @@ -42,7 +44,7 @@ class UserMethods: diff = round(due - time.time()) if diff <= 3: # Flood waits below 3 seconds are "ignored" self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None) - elif diff <= self.flood_sleep_threshold: + elif diff <= flood_sleep_threshold: self._log[__name__].info(*_fmt_flood(diff, r, early=True)) await asyncio.sleep(diff) self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None) From 45ed6658fe6829f0efe6196dc91adb797e802daa Mon Sep 17 00:00:00 2001 From: ZubAnt Date: Sun, 15 Aug 2021 09:01:25 +0300 Subject: [PATCH 013/256] Fix add_admins property of custom.ParticipantPermissions (#3132) Closes #3131. --- telethon/tl/custom/participantpermissions.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/telethon/tl/custom/participantpermissions.py b/telethon/tl/custom/participantpermissions.py index c59a05ed..d3719778 100644 --- a/telethon/tl/custom/participantpermissions.py +++ b/telethon/tl/custom/participantpermissions.py @@ -92,9 +92,12 @@ class ParticipantPermissions: Whether the administrator can add new administrators with the same or less permissions than them. """ - if not self.is_admin or (self.is_chat and not self.is_creator): + if not self.is_admin: return False + if self.is_chat: + return self.is_creator + return self.participant.admin_rights.add_admins ban_users = property(**_admin_prop('ban_users', """ From 6a1f29d953bfd34c4d235888f4684cf222437cd8 Mon Sep 17 00:00:00 2001 From: Shrimadhav U K Date: Fri, 20 Aug 2021 15:19:12 +0530 Subject: [PATCH 014/256] Add new known RPC errors (#3134) --- telethon_generator/data/errors.csv | 5 +++++ telethon_generator/data/methods.csv | 11 ++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/telethon_generator/data/errors.csv b/telethon_generator/data/errors.csv index 498006b9..e50db207 100644 --- a/telethon_generator/data/errors.csv +++ b/telethon_generator/data/errors.csv @@ -133,6 +133,7 @@ FILE_PART_X_MISSING,400,Part {which} of the file is missing from storage FILE_REFERENCE_EMPTY,400,The file reference must exist to access the media and it cannot be empty FILE_REFERENCE_EXPIRED,400,The file reference has expired and is no longer valid or it belongs to self-destructing media and cannot be resent FILE_REFERENCE_INVALID,400,The file reference is invalid or you can't do that operation on such message +FILE_TITLE_EMPTY,400, FIRSTNAME_INVALID,400,The first name is invalid FLOOD_TEST_PHONE_WAIT_X,420,A wait of {seconds} seconds is required in the test servers FLOOD_WAIT_X,420,A wait of {seconds} seconds is required @@ -231,6 +232,7 @@ PASSWORD_EMPTY,400,The provided password is empty PASSWORD_HASH_INVALID,400,The password (and thus its hash value) you entered is invalid PASSWORD_MISSING,400,The account must have 2-factor authentication enabled (a password) before this method can be used PASSWORD_RECOVERY_EXPIRED,400, +PASSWORD_RECOVERY_NA,400, PASSWORD_REQUIRED,400,The account must have 2-factor authentication enabled (a password) before this method can be used PASSWORD_TOO_FRESH_X,400,The password was added too recently and {seconds} seconds must pass before using the method PAYMENT_PROVIDER_INVALID,400,The payment provider was not recognised or its token was invalid @@ -263,16 +265,19 @@ PHOTO_INVALID_DIMENSIONS,400,The photo dimensions are invalid (hint: `pip instal PHOTO_SAVE_FILE_INVALID,400,The photo you tried to send cannot be saved by Telegram. A reason may be that it exceeds 10MB. Try resizing it locally PHOTO_THUMB_URL_EMPTY,400,The URL used as a thumbnail appears to be empty or has caused another HTTP error PIN_RESTRICTED,400,You can't pin messages in private chats with other people +PINNED_DIALOGS_TOO_MUCH,400, POLL_ANSWERS_INVALID,400,The poll did not have enough answers or had too many POLL_OPTION_DUPLICATE,400,A duplicate option was sent in the same poll POLL_OPTION_INVALID,400,A poll option used invalid data (the data may be too long) POLL_QUESTION_INVALID,400,The poll question was either empty or too long POLL_UNSUPPORTED,400,This layer does not support polls in the issued method +POLL_VOTE_REQUIRED,403, PREVIOUS_CHAT_IMPORT_ACTIVE_WAIT_XMIN,406,"Similar to a flood wait, must wait {minutes} minutes" PRIVACY_KEY_INVALID,400,The privacy key is invalid PRIVACY_TOO_LONG,400,Cannot add that many entities in a single request PRIVACY_VALUE_INVALID,400,The privacy value is invalid PTS_CHANGE_EMPTY,500,No PTS change +PUBLIC_KEY_REQUIRED,400, QUERY_ID_EMPTY,400,The query ID is empty QUERY_ID_INVALID,400,The query ID is invalid QUERY_TOO_SHORT,400,The query string is too short diff --git a/telethon_generator/data/methods.csv b/telethon_generator/data/methods.csv index 9e92a452..381745a5 100644 --- a/telethon_generator/data/methods.csv +++ b/telethon_generator/data/methods.csv @@ -12,7 +12,7 @@ account.deleteSecureValue,user, account.finishTakeoutSession,user, account.getAccountTTL,user, account.getAllSecureValues,user, -account.getAuthorizationForm,user, +account.getAuthorizationForm,user,PUBLIC_KEY_REQUIRED account.getAuthorizations,user, account.getAutoDownloadSettings,user, account.getContactSignUpNotification,user, @@ -79,7 +79,7 @@ auth.importBotAuthorization,both,ACCESS_TOKEN_EXPIRED ACCESS_TOKEN_INVALID API_I auth.importLoginToken,user,AUTH_TOKEN_ALREADY_ACCEPTED AUTH_TOKEN_EXPIRED AUTH_TOKEN_INVALID auth.logOut,both, auth.recoverPassword,user,CODE_EMPTY NEW_SETTINGS_INVALID -auth.requestPasswordRecovery,user,PASSWORD_EMPTY +auth.requestPasswordRecovery,user,PASSWORD_EMPTY PASSWORD_RECOVERY_NA auth.resendCode,user,PHONE_NUMBER_INVALID auth.resetAuthorizations,user,TIMEOUT auth.sendCode,user,API_ID_INVALID API_ID_PUBLISHED_FLOOD AUTH_RESTART INPUT_REQUEST_TOO_LONG PHONE_NUMBER_APP_SIGNUP_FORBIDDEN PHONE_NUMBER_BANNED PHONE_NUMBER_FLOOD PHONE_NUMBER_INVALID PHONE_PASSWORD_FLOOD PHONE_PASSWORD_PROTECTED @@ -89,6 +89,7 @@ bots.answerWebhookJSONQuery,bot,QUERY_ID_INVALID USER_BOT_INVALID bots.sendCustomRequest,bot,USER_BOT_INVALID bots.setBotCommands,bot,BOT_COMMAND_DESCRIPTION_INVALID BOT_COMMAND_INVALID LANG_CODE_INVALID channels.checkUsername,user,CHANNEL_INVALID CHAT_ID_INVALID USERNAME_INVALID +channels.convertToGigagroup,user,PARTICIPANTS_TOO_FEW channels.createChannel,user,CHAT_TITLE_EMPTY USER_RESTRICTED channels.deleteChannel,user,CHANNEL_INVALID CHANNEL_PRIVATE channels.deleteHistory,user, @@ -231,7 +232,7 @@ messages.getPeerDialogs,user,CHANNEL_PRIVATE PEER_ID_INVALID messages.getPeerSettings,user,CHANNEL_INVALID PEER_ID_INVALID messages.getPinnedDialogs,user, messages.getPollResults,user, -messages.getPollVotes,user,BROADCAST_FORBIDDEN +messages.getPollVotes,user,BROADCAST_FORBIDDEN POLL_VOTE_REQUIRED messages.getRecentLocations,user, messages.getRecentStickers,user, messages.getSavedGifs,user, @@ -288,12 +289,12 @@ messages.setBotShippingResults,both,QUERY_ID_INVALID messages.setEncryptedTyping,user,CHAT_ID_INVALID messages.setGameScore,bot,PEER_ID_INVALID USER_BOT_REQUIRED messages.setHistoryTTL,user,CHAT_NOT_MODIFIED TTL_PERIOD_INVALID -messages.setInlineBotResults,bot,ARTICLE_TITLE_EMPTY AUDIO_CONTENT_URL_EMPTY AUDIO_TITLE_EMPTY BUTTON_DATA_INVALID BUTTON_TYPE_INVALID BUTTON_URL_INVALID DOCUMENT_INVALID FILE_CONTENT_TYPE_INVALID GIF_CONTENT_TYPE_INVALID MESSAGE_EMPTY NEXT_OFFSET_INVALID PHOTO_CONTENT_TYPE_INVALID PHOTO_CONTENT_URL_EMPTY PHOTO_THUMB_URL_EMPTY QUERY_ID_INVALID REPLY_MARKUP_INVALID RESULT_TYPE_INVALID SEND_MESSAGE_MEDIA_INVALID SEND_MESSAGE_TYPE_INVALID START_PARAM_INVALID STICKER_DOCUMENT_INVALID USER_BOT_INVALID VIDEO_TITLE_EMPTY WEBDOCUMENT_MIME_INVALID WEBDOCUMENT_URL_INVALID +messages.setInlineBotResults,bot,ARTICLE_TITLE_EMPTY AUDIO_CONTENT_URL_EMPTY AUDIO_TITLE_EMPTY BUTTON_DATA_INVALID BUTTON_TYPE_INVALID BUTTON_URL_INVALID DOCUMENT_INVALID FILE_CONTENT_TYPE_INVALID FILE_TITLE_EMPTY GIF_CONTENT_TYPE_INVALID MESSAGE_EMPTY NEXT_OFFSET_INVALID PHOTO_CONTENT_TYPE_INVALID PHOTO_CONTENT_URL_EMPTY PHOTO_THUMB_URL_EMPTY QUERY_ID_INVALID REPLY_MARKUP_INVALID RESULT_TYPE_INVALID SEND_MESSAGE_MEDIA_INVALID SEND_MESSAGE_TYPE_INVALID START_PARAM_INVALID STICKER_DOCUMENT_INVALID USER_BOT_INVALID VIDEO_TITLE_EMPTY WEBDOCUMENT_MIME_INVALID WEBDOCUMENT_URL_INVALID messages.setInlineGameScore,bot,MESSAGE_ID_INVALID USER_BOT_REQUIRED messages.setTyping,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ID_INVALID CHAT_WRITE_FORBIDDEN PEER_ID_INVALID USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT messages.startBot,user,BOT_INVALID PEER_ID_INVALID START_PARAM_EMPTY START_PARAM_INVALID messages.startHistoryImport,user,IMPORT_ID_INVALID -messages.toggleDialogPin,user,PEER_ID_INVALID +messages.toggleDialogPin,user,PEER_ID_INVALID PINNED_DIALOGS_TOO_MUCH messages.toggleStickerSets,user, messages.uninstallStickerSet,user,STICKERSET_INVALID messages.updateDialogFilter,user, From bc6bcd31ad0c46a26e8597689c69de6c1711d14a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 22 Aug 2021 13:09:57 +0200 Subject: [PATCH 015/256] Fix InlineQuery.event.geo returning None Closes #3136. --- telethon/events/inlinequery.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/telethon/events/inlinequery.py b/telethon/events/inlinequery.py index ad3dbcf6..75cfcb01 100644 --- a/telethon/events/inlinequery.py +++ b/telethon/events/inlinequery.py @@ -130,7 +130,7 @@ class InlineQuery(EventBuilder): and the user's device is able to send it, this will return the :tl:`GeoPoint` with the position of the user. """ - return + return self.query.geo @property def builder(self): @@ -174,9 +174,9 @@ class InlineQuery(EventBuilder): gallery (`bool`, optional): Whether the results should show as a gallery (grid) or not. - + next_offset (`str`, optional): - The offset the client will send when the user scrolls the + The offset the client will send when the user scrolls the results and it repeats the request. private (`bool`, optional): From 9285e50c63a918975da5d1f8cc4940928886dadc Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 22 Aug 2021 13:24:02 +0200 Subject: [PATCH 016/256] Handle non-user bans when iterating banned participants Closes #3105. --- telethon/client/chats.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/telethon/client/chats.py b/telethon/client/chats.py index ca4e771b..ef7a10cb 100644 --- a/telethon/client/chats.py +++ b/telethon/client/chats.py @@ -233,6 +233,9 @@ class _ParticipantsIter(RequestIter): for participant in participants.participants: if isinstance(participant, types.ChannelParticipantBanned): + if not isinstance(participant.peer, types.PeerUser): + # May have the entire channel banned. See #3105. + continue user_id = participant.peer.user_id else: user_id = participant.user_id From 49713b27844889272bfdbfa5bcde35c5dd2c74a9 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 22 Aug 2021 13:38:54 +0200 Subject: [PATCH 017/256] Wrap requests in InvokeWithoutUpdatesRequest if no event handlers Closes #1270. --- telethon/client/users.py | 3 +++ telethon/events/callbackquery.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/telethon/client/users.py b/telethon/client/users.py index 49408896..7881cbd9 100644 --- a/telethon/client/users.py +++ b/telethon/client/users.py @@ -51,6 +51,9 @@ class UserMethods: else: raise errors.FloodWaitError(request=r, capture=diff) + if not self._event_builders and not self._conversations: + r = functions.InvokeWithoutUpdatesRequest(r) + request_index = 0 last_error = None self._last_request = time.time() diff --git a/telethon/events/callbackquery.py b/telethon/events/callbackquery.py index d1558d21..94e03b7b 100644 --- a/telethon/events/callbackquery.py +++ b/telethon/events/callbackquery.py @@ -135,7 +135,7 @@ class CallbackQuery(EventBuilder): The object returned by the ``data=`` parameter when creating the event builder, if any. Similar to ``pattern_match`` for the new message event. - + pattern_match (`obj`, optional): Alias for ``data_match``. """ From 1e6be28e4b2cafd60d94eab881d966ef809630fa Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 22 Aug 2021 13:46:26 +0200 Subject: [PATCH 018/256] Fix pin_message not returning Message on PMs --- telethon/client/messages.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/telethon/client/messages.py b/telethon/client/messages.py index 1cb829c0..2a6a283e 100644 --- a/telethon/client/messages.py +++ b/telethon/client/messages.py @@ -1360,7 +1360,7 @@ class MessageMethods: notify (`bool`, optional): Whether the pin should notify people or not. - + pm_oneside (`bool`, optional): Whether the message should be pinned for everyone or not. By default it has the opposite behaviour of official clients, @@ -1421,12 +1421,11 @@ class MessageMethods: ) result = await self(request) - # Unpinning does not produce a service message - if unpin: - return - - # Pinning in User chats (just with yourself really) does not produce a service message - if helpers._entity_type(entity) == helpers._EntityType.USER: + # Unpinning does not produce a service message. + # Pinning a message that was already pinned also produces no service message. + # Pinning a message in your own chat does not produce a service message, + # but pinning on a private conversation with someone else does. + if unpin or not result.updates: return # Pinning a message that doesn't exist would RPC-error earlier From 2ea3153cd50dc6d8c6bd1771c380dea70726302a Mon Sep 17 00:00:00 2001 From: Newbyte Date: Sat, 28 Aug 2021 00:18:22 +0200 Subject: [PATCH 019/256] Update docs to reflect current length of login codes in test servers (#3140) --- readthedocs/developing/test-servers.rst | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/readthedocs/developing/test-servers.rst b/readthedocs/developing/test-servers.rst index d93190a4..513eed54 100644 --- a/readthedocs/developing/test-servers.rst +++ b/readthedocs/developing/test-servers.rst @@ -25,13 +25,17 @@ so don't store sensitive data here. Valid phone numbers are ``99966XYYYY``, where ``X`` is the ``dc_id`` and ``YYYY`` is any number you want, for example, ``1234`` in ``dc_id = 2`` would -be ``9996621234``. The code sent by Telegram will be ``dc_id`` repeated six -times, in this case, ``222222`` so we can hardcode that: +be ``9996621234``. The code sent by Telegram will be ``dc_id`` repeated five +times, in this case, ``22222`` so we can hardcode that: .. code-block:: python client = TelegramClient(None, api_id, api_hash) client.session.set_dc(2, '149.154.167.40', 80) client.start( - phone='9996621234', code_callback=lambda: '222222' + phone='9996621234', code_callback=lambda: '22222' ) + +Note that Telegram has changed the length of login codes multiple times in the +past, so if ``dc_id`` repeated five times does not work, try repeating it six +times. From 0a4b827d8ec50d17bfecb03c3c3f75420e810351 Mon Sep 17 00:00:00 2001 From: Shrimadhav U K Date: Sat, 28 Aug 2021 03:48:37 +0530 Subject: [PATCH 020/256] Document new known RPC errors (#3137) --- telethon_generator/data/errors.csv | 2 ++ telethon_generator/data/methods.csv | 1 + 2 files changed, 3 insertions(+) diff --git a/telethon_generator/data/errors.csv b/telethon_generator/data/errors.csv index e50db207..6e50f683 100644 --- a/telethon_generator/data/errors.csv +++ b/telethon_generator/data/errors.csv @@ -101,6 +101,8 @@ DOCUMENT_INVALID,400,The document file was invalid and can't be used in inline m EMAIL_HASH_EXPIRED,400,The email hash expired and cannot be used to verify it EMAIL_INVALID,400,The given email is invalid EMAIL_UNCONFIRMED_X,400,"Email unconfirmed, the length of the code must be {code_length}" +EMOJI_INVALID,400, +EMOJI_NOT_MODIFIED,400, EMOTICON_EMPTY,400,The emoticon field cannot be empty EMOTICON_INVALID,400,The specified emoticon cannot be used or was not a emoticon EMOTICON_STICKERPACK_MISSING,400,The emoticon sticker pack you are trying to get is missing diff --git a/telethon_generator/data/methods.csv b/telethon_generator/data/methods.csv index 381745a5..9066202d 100644 --- a/telethon_generator/data/methods.csv +++ b/telethon_generator/data/methods.csv @@ -286,6 +286,7 @@ messages.sendVote,user,MESSAGE_POLL_CLOSED OPTION_INVALID messages.setBotCallbackAnswer,both,QUERY_ID_INVALID URL_INVALID messages.setBotPrecheckoutResults,both,ERROR_TEXT_EMPTY messages.setBotShippingResults,both,QUERY_ID_INVALID +messages.setChatTheme,user,EMOJI_INVALID EMOJI_NOT_MODIFIED messages.setEncryptedTyping,user,CHAT_ID_INVALID messages.setGameScore,bot,PEER_ID_INVALID USER_BOT_REQUIRED messages.setHistoryTTL,user,CHAT_NOT_MODIFIED TTL_PERIOD_INVALID From 9830c4e02b12e0a0bd39bd4f47da6f22f1f5e8ea Mon Sep 17 00:00:00 2001 From: Devesh Pal Date: Sun, 29 Aug 2021 15:19:52 +0530 Subject: [PATCH 021/256] Add Button.buy and Button.game (#3141) --- telethon/tl/custom/button.py | 37 +++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/telethon/tl/custom/button.py b/telethon/tl/custom/button.py index e8003992..0ed9a43d 100644 --- a/telethon/tl/custom/button.py +++ b/telethon/tl/custom/button.py @@ -20,7 +20,7 @@ class Button: instances instead making them yourself (i.e. don't do ``Button(...)`` but instead use methods line `Button.inline(...) ` etc. - You can use `inline`, `switch_inline`, `url` and `auth` + You can use `inline`, `switch_inline`, `url`, `auth`, `buy` and `game` together to create inline buttons (under the message). You can use `text`, `request_location`, `request_phone` and `request_poll` @@ -49,7 +49,9 @@ class Button: Returns `True` if the button belongs to an inline keyboard. """ return isinstance(button, ( + types.KeyboardButtonBuy, types.KeyboardButtonCallback, + types.KeyboardButtonGame, types.KeyboardButtonSwitchInline, types.KeyboardButtonUrl, types.InputKeyboardButtonUrlAuth @@ -266,8 +268,41 @@ class Button: Args: placeholder (str): text to show the user at typing place of message. + + If the placeholder is too long, Telegram applications will + crop the text (for example, to 64 characters and adding an + ellipsis (…) character as the 65th). """ return types.ReplyKeyboardForceReply( single_use=single_use, selective=selective, placeholder=placeholder) + + @staticmethod + def buy(text): + """ + Creates a new inline button to buy a product. + + This can only be used when sending files of type + :tl:`InputMediaInvoice`, and must be the first button. + + If the button is not specified, Telegram will automatically + add the button to the message. See the + `Payments API `__ + documentation for more information. + """ + return types.KeyboardButtonBuy(text) + + @staticmethod + def game(text): + """ + Creates a new inline button to start playing a game. + + This should be used when sending files of type + :tl:`InputMediaGame`, and must be the first button. + + See the + `Games `__ + documentation for more information on using games. + """ + return types.KeyboardButtonGame(text) From 828cf2dcad894be433d71393b414df6b4bc1fb14 Mon Sep 17 00:00:00 2001 From: painor Date: Sun, 29 Aug 2021 10:50:48 +0100 Subject: [PATCH 022/256] Include "chat" attribute in processing entities (#3133) Requests like checkChatInvite return a chatInviteAlready, which has a "chat" attribute similar to the "user" attribute other requests have. --- telethon/sessions/memory.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/telethon/sessions/memory.py b/telethon/sessions/memory.py index 881622a6..1b1a6bfb 100644 --- a/telethon/sessions/memory.py +++ b/telethon/sessions/memory.py @@ -130,6 +130,8 @@ class MemorySession(Session): entities = [] if hasattr(tlo, 'user'): entities.append(tlo.user) + if hasattr(tlo, 'chat'): + entities.append(tlo.chat) if hasattr(tlo, 'chats') and utils.is_list_like(tlo.chats): entities.extend(tlo.chats) if hasattr(tlo, 'users') and utils.is_list_like(tlo.users): From befba11657d37760d4096ec85ec66af6eb8d4cb7 Mon Sep 17 00:00:00 2001 From: Devesh Pal Date: Sun, 29 Aug 2021 15:23:06 +0530 Subject: [PATCH 023/256] Add support for scheduled messages in iter_messages (#3127) --- telethon/client/messages.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/telethon/client/messages.py b/telethon/client/messages.py index 2a6a283e..01011b58 100644 --- a/telethon/client/messages.py +++ b/telethon/client/messages.py @@ -19,7 +19,8 @@ class _MessagesIter(RequestIter): """ async def _init( self, entity, offset_id, min_id, max_id, - from_user, offset_date, add_offset, filter, search, reply_to + from_user, offset_date, add_offset, filter, search, reply_to, + scheduled ): # Note that entity being `None` will perform a global search. if entity: @@ -84,6 +85,11 @@ class _MessagesIter(RequestIter): offset_id=offset_id, limit=1 ) + elif scheduled: + self.request = functions.messages.GetScheduledHistoryRequest( + peer=entity, + hash=0 + ) elif reply_to is not None: self.request = functions.messages.GetRepliesRequest( peer=self.entity, @@ -336,7 +342,8 @@ class MessageMethods: wait_time: float = None, ids: 'typing.Union[int, typing.Sequence[int]]' = None, reverse: bool = False, - reply_to: int = None + reply_to: int = None, + scheduled: bool = False ) -> 'typing.Union[_MessagesIter, _IDsIter]': """ Iterator over the messages for the given chat. @@ -463,6 +470,10 @@ class MessageMethods: a message and replies to it itself, that reply will not be included in the results. + scheduled (`bool`, optional): + If set to `True`, messages which are scheduled will be returned. + All other parameter will be ignored for this, except `entity`. + Yields Instances of `Message `. @@ -521,7 +532,8 @@ class MessageMethods: add_offset=add_offset, filter=filter, search=search, - reply_to=reply_to + reply_to=reply_to, + scheduled=scheduled ) async def get_messages(self: 'TelegramClient', *args, **kwargs) -> 'hints.TotalList': From 2cb6cd5dad52f3cf03381e2b769435eb6b92c830 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 29 Aug 2021 11:36:08 +0200 Subject: [PATCH 024/256] Change the way no_updates mode is enabled See discussion on https://github.com/LonamiWebs/Telethon/commit/49713b2. The problem with the automatic approach is that some scripts may do some "fancier" things with the way they register updates, so it was prone to failure (a handler could be added but since the last request was without updates, nothing would be received). This new approach is a bit more annoying to opt-into but also more explicit. --- telethon/client/telegrambaseclient.py | 14 +++++++++++++- telethon/client/updates.py | 11 +++++++++++ telethon/client/users.py | 2 +- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/telethon/client/telegrambaseclient.py b/telethon/client/telegrambaseclient.py index e8192d73..494daf9c 100644 --- a/telethon/client/telegrambaseclient.py +++ b/telethon/client/telegrambaseclient.py @@ -199,6 +199,15 @@ class TelegramBaseClient(abc.ABC): If a `str` is given, it'll be passed to `logging.getLogger()`. If a `logging.Logger` is given, it'll be used directly. If something else or nothing is given, the default logger will be used. + + receive_updates (`bool`, optional): + Whether the client will receive updates or not. By default, updates + will be received from Telegram as they occur. + + Turning this off means that Telegram will not send updates at all + so event handlers, conversations, and QR login will not work. + However, certain scripts don't need updates, so this will reduce + the amount of bandwidth used. """ # Current TelegramClient version @@ -234,7 +243,9 @@ class TelegramBaseClient(abc.ABC): lang_code: str = 'en', system_lang_code: str = 'en', loop: asyncio.AbstractEventLoop = None, - base_logger: typing.Union[str, logging.Logger] = None): + base_logger: typing.Union[str, logging.Logger] = None, + receive_updates: bool = True + ): if not api_id or not api_hash: raise ValueError( "Your API ID or Hash cannot be empty or None. " @@ -388,6 +399,7 @@ class TelegramBaseClient(abc.ABC): self._updates_handle = None self._last_request = time.time() self._channel_pts = {} + self._no_updates = not receive_updates if sequential_updates: self._updates_queue = asyncio.Queue() diff --git a/telethon/client/updates.py b/telethon/client/updates.py index 5bdf5893..bcc983f3 100644 --- a/telethon/client/updates.py +++ b/telethon/client/updates.py @@ -32,6 +32,17 @@ class UpdateMethods: finally: await self.disconnect() + async def set_receive_updates(self: 'TelegramClient', receive_updates): + """ + Change the value of `receive_updates`. + + This is an `async` method, because in order for Telegram to start + sending updates again, a request must be made. + """ + self._no_updates = not receive_updates + if receive_updates: + await self(functions.updates.GetStateRequest()) + def run_until_disconnected(self: 'TelegramClient'): """ Runs the event loop until the library is disconnected. diff --git a/telethon/client/users.py b/telethon/client/users.py index 7881cbd9..22db969e 100644 --- a/telethon/client/users.py +++ b/telethon/client/users.py @@ -51,7 +51,7 @@ class UserMethods: else: raise errors.FloodWaitError(request=r, capture=diff) - if not self._event_builders and not self._conversations: + if self._no_updates: r = functions.InvokeWithoutUpdatesRequest(r) request_index = 0 From 8c56f95252cac7db0008a8fa377f21e6e0f54ed9 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 29 Aug 2021 12:13:55 +0200 Subject: [PATCH 025/256] Include full request on the cause of RPC errors Closes #3110, fixes #3109. --- telethon/errors/rpcbaseerrors.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/telethon/errors/rpcbaseerrors.py b/telethon/errors/rpcbaseerrors.py index f6685aeb..eecc974f 100644 --- a/telethon/errors/rpcbaseerrors.py +++ b/telethon/errors/rpcbaseerrors.py @@ -1,3 +1,15 @@ +from ..tl import functions + +_NESTS_QUERY = ( + functions.InvokeAfterMsgRequest, + functions.InvokeAfterMsgsRequest, + functions.InitConnectionRequest, + functions.InvokeWithLayerRequest, + functions.InvokeWithoutUpdatesRequest, + functions.InvokeWithMessagesRangeRequest, + functions.InvokeWithTakeoutRequest, +) + class RPCError(Exception): """Base class for all Remote Procedure Call errors.""" code = None @@ -13,7 +25,15 @@ class RPCError(Exception): @staticmethod def _fmt_request(request): - return ' (caused by {})'.format(request.__class__.__name__) + n = 0 + reason = '' + while isinstance(request, _NESTS_QUERY): + n += 1 + reason += request.__class__.__name__ + '(' + request = request.query + reason += request.__class__.__name__ + ')' * n + + return ' (caused by {})'.format(reason) def __reduce__(self): return type(self), (self.request, self.message, self.code) From 022c1db33f29f409132ee3245d41bab9ced30adb Mon Sep 17 00:00:00 2001 From: Yusuf_M_Thon_iD <32301831+Sunda001@users.noreply.github.com> Date: Mon, 30 Aug 2021 22:33:46 +0700 Subject: [PATCH 026/256] Update ChatAction to include MessageActionGameScore (#1651) --- telethon/events/chataction.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/telethon/events/chataction.py b/telethon/events/chataction.py index dfd361e0..8261d6e1 100644 --- a/telethon/events/chataction.py +++ b/telethon/events/chataction.py @@ -11,6 +11,7 @@ class ChatAction(EventBuilder): * Whenever a new chat is created. * Whenever a chat's title or photo is changed or removed. * Whenever a new message is pinned. + * Whenever a user scores in a game. * Whenever a user joins or is added to the group. * Whenever a user is removed or leaves a group if it has less than 50 members or the removed user was a bot. @@ -29,6 +30,7 @@ class ChatAction(EventBuilder): if event.user_joined: await event.reply('Welcome to the group!') """ + @classmethod def build(cls, update, others=None, self_id=None): # Rely on specific pin updates for unpins, but otherwise ignore them @@ -102,6 +104,9 @@ class ChatAction(EventBuilder): elif isinstance(action, types.MessageActionPinMessage) and msg.reply_to: return cls.Event(msg, pin_ids=[msg.reply_to_msg_id]) + elif isinstance(action, types.MessageActionGameScore): + return cls.Event(msg, + new_score=action.score) class Event(EventCommon): """ @@ -137,13 +142,17 @@ class ChatAction(EventBuilder): new_title (`str`, optional): The new title string for the chat, if applicable. + + new_score (`str`, optional): + The new score string for the game, if applicable. unpin (`bool`): `True` if the existing pin gets unpinned. """ + def __init__(self, where, new_photo=None, added_by=None, kicked_by=None, created=None, - users=None, new_title=None, pin_ids=None, pin=None): + users=None, new_title=None, pin_ids=None, pin=None, new_score=None): if isinstance(where, types.MessageService): self.action_message = where where = where.peer_id @@ -193,6 +202,7 @@ class ChatAction(EventBuilder): self._users = None self._input_users = None self.new_title = new_title + self.new_score = new_score self.unpin = not pin def _set_client(self, client): From 2182e7f6f199735709a6e20da6c8b4c6145795b1 Mon Sep 17 00:00:00 2001 From: Shrimadhav U K Date: Tue, 31 Aug 2021 18:52:56 +0530 Subject: [PATCH 027/256] Update to layer 132 (#3142) --- telethon_generator/data/api.tl | 41 +++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/telethon_generator/data/api.tl b/telethon_generator/data/api.tl index b001ef20..82e59c8b 100644 --- a/telethon_generator/data/api.tl +++ b/telethon_generator/data/api.tl @@ -92,7 +92,7 @@ inputPhotoFileLocation#40181ffe id:long access_hash:long file_reference:bytes th inputPhotoLegacyFileLocation#d83466f3 id:long access_hash:long file_reference:bytes volume_id:long local_id:int secret:long = InputFileLocation; inputPeerPhotoFileLocation#37257e99 flags:# big:flags.0?true peer:InputPeer photo_id:long = InputFileLocation; inputStickerSetThumb#9d84f3db stickerset:InputStickerSet thumb_version:int = InputFileLocation; -inputGroupCallStream#bba51639 call:InputGroupCall time_ms:long scale:int = InputFileLocation; +inputGroupCallStream#598a92a flags:# call:InputGroupCall time_ms:long scale:int video_channel:flags.0?int video_quality:flags.0?int = InputFileLocation; peerUser#9db1bc6d user_id:int = Peer; peerChat#bad0e5bb chat_id:int = Peer; @@ -128,8 +128,8 @@ chatForbidden#7328bdb id:int title:string = Chat; channel#d31a961e flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true fake:flags.25?true gigagroup:flags.26?true id:int access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int version:int restriction_reason:flags.9?Vector admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int = Chat; channelForbidden#289da732 flags:# broadcast:flags.5?true megagroup:flags.8?true id:int access_hash:long title:string until_date:flags.16?int = Chat; -chatFull#8a1e2983 flags:# can_set_username:flags.7?true has_scheduled:flags.8?true id:int about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer = ChatFull; -channelFull#548c3f93 flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?int location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer = ChatFull; +chatFull#49a0a5d9 flags:# can_set_username:flags.7?true has_scheduled:flags.8?true id:int about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer theme_emoticon:flags.16?string = ChatFull; +channelFull#2f532f3c flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?int location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string = ChatFull; chatParticipant#c8d7493e user_id:int inviter_id:int date:int = ChatParticipant; chatParticipantCreator#da13538a user_id:int = ChatParticipant; @@ -187,6 +187,7 @@ messageActionGroupCall#7a0d7f42 flags:# call:InputGroupCall duration:flags.0?int messageActionInviteToGroupCall#76b9f11a call:InputGroupCall users:Vector = MessageAction; messageActionSetMessagesTTL#aa1afbfd period:int = MessageAction; messageActionGroupCallScheduled#b3a07661 call:InputGroupCall schedule_date:int = MessageAction; +messageActionSetChatTheme#aa786345 emoticon:string = MessageAction; dialog#2c171f72 flags:# pinned:flags.2?true unread_mark:flags.3?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int = Dialog; dialogFolder#71bd134c flags:# pinned:flags.2?true folder:Folder peer:Peer top_message:int unread_muted_peers_count:int unread_unmuted_peers_count:int unread_muted_messages_count:int unread_unmuted_messages_count:int = Dialog; @@ -234,7 +235,7 @@ inputReportReasonCopyright#9b89f93a = ReportReason; inputReportReasonGeoIrrelevant#dbd4feed = ReportReason; inputReportReasonFake#f5ddd6e7 = ReportReason; -userFull#139a9a77 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true user:User about:flags.1?string settings:PeerSettings profile_photo:flags.2?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int = UserFull; +userFull#d697ff05 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true user:User about:flags.1?string settings:PeerSettings profile_photo:flags.2?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string = UserFull; contact#f911c994 user_id:int mutual:Bool = Contact; @@ -465,6 +466,7 @@ sendMessageRecordRoundAction#88f27fbc = SendMessageAction; sendMessageUploadRoundAction#243e1c66 progress:int = SendMessageAction; speakingInGroupCallAction#d92c2285 = SendMessageAction; sendMessageHistoryImportAction#dbda9246 progress:int = SendMessageAction; +sendMessageChooseStickerAction#b05ac6b1 = SendMessageAction; contacts.found#b3134d9d my_results:Vector results:Vector chats:Vector users:Vector = contacts.Found; @@ -1116,7 +1118,7 @@ restrictionReason#d072acb4 platform:string reason:string text:string = Restricti inputTheme#3c5693e9 id:long access_hash:long = InputTheme; inputThemeSlug#f5890df1 slug:string = InputTheme; -theme#28f1114 flags:# creator:flags.0?true default:flags.1?true id:long access_hash:long slug:string title:string document:flags.2?Document settings:flags.3?ThemeSettings installs_count:int = Theme; +theme#e802b8dc flags:# creator:flags.0?true default:flags.1?true for_chat:flags.5?true id:long access_hash:long slug:string title:string document:flags.2?Document settings:flags.3?ThemeSettings installs_count:flags.4?int = Theme; account.themesNotModified#f41eb622 = account.Themes; account.themes#7f676421 hash:int themes:Vector = account.Themes; @@ -1135,9 +1137,9 @@ baseThemeNight#b7b31ea8 = BaseTheme; baseThemeTinted#6d5f77ee = BaseTheme; baseThemeArctic#5b11125a = BaseTheme; -inputThemeSettings#bd507cd1 flags:# base_theme:BaseTheme accent_color:int message_top_color:flags.0?int message_bottom_color:flags.0?int wallpaper:flags.1?InputWallPaper wallpaper_settings:flags.1?WallPaperSettings = InputThemeSettings; +inputThemeSettings#ff38f912 flags:# message_colors_animated:flags.2?true base_theme:BaseTheme accent_color:int message_colors:flags.0?Vector wallpaper:flags.1?InputWallPaper wallpaper_settings:flags.1?WallPaperSettings = InputThemeSettings; -themeSettings#9c14984a flags:# base_theme:BaseTheme accent_color:int message_top_color:flags.0?int message_bottom_color:flags.0?int wallpaper:flags.1?WallPaper = ThemeSettings; +themeSettings#8db4e76c flags:# message_colors_animated:flags.2?true base_theme:BaseTheme accent_color:int message_colors:flags.0?Vector wallpaper:flags.1?WallPaper = ThemeSettings; webPageAttributeTheme#54b56617 flags:# documents:flags.0?Vector settings:flags.1?ThemeSettings = WebPageAttribute; @@ -1195,7 +1197,7 @@ messageViews#455b853d flags:# views:flags.0?int forwards:flags.1?int replies:fla messages.messageViews#b6c4f543 views:Vector chats:Vector users:Vector = messages.MessageViews; -messages.discussionMessage#f5dd8f9d flags:# messages:Vector max_id:flags.0?int read_inbox_max_id:flags.1?int read_outbox_max_id:flags.2?int chats:Vector users:Vector = messages.DiscussionMessage; +messages.discussionMessage#a6341782 flags:# messages:Vector max_id:flags.0?int read_inbox_max_id:flags.1?int read_outbox_max_id:flags.2?int unread_count:int chats:Vector users:Vector = messages.DiscussionMessage; messageReplyHeader#a6d57763 flags:# reply_to_msg_id:int reply_to_peer_id:flags.0?Peer reply_to_top_id:flags.1?int = MessageReplyHeader; @@ -1206,7 +1208,7 @@ peerBlocked#e8fd8014 peer_id:Peer date:int = PeerBlocked; stats.messageStats#8999f295 views_graph:StatsGraph = stats.MessageStats; groupCallDiscarded#7780bcb4 id:long access_hash:long duration:int = GroupCall; -groupCall#d597650c flags:# join_muted:flags.1?true can_change_join_muted:flags.2?true join_date_asc:flags.6?true schedule_start_subscribed:flags.8?true can_start_video:flags.9?true id:long access_hash:long participants_count:int title:flags.3?string stream_dc_id:flags.4?int record_start_date:flags.5?int schedule_date:flags.7?int unmuted_video_count:flags.10?int unmuted_video_limit:int version:int = GroupCall; +groupCall#d597650c flags:# join_muted:flags.1?true can_change_join_muted:flags.2?true join_date_asc:flags.6?true schedule_start_subscribed:flags.8?true can_start_video:flags.9?true record_video_active:flags.11?true id:long access_hash:long participants_count:int title:flags.3?string stream_dc_id:flags.4?int record_start_date:flags.5?int schedule_date:flags.7?int unmuted_video_count:flags.10?int unmuted_video_limit:int version:int = GroupCall; inputGroupCall#d8aa840f id:long access_hash:long = InputGroupCall; @@ -1265,6 +1267,15 @@ account.resetPasswordFailedWait#e3779861 retry_date:int = account.ResetPasswordR account.resetPasswordRequestedWait#e9effc7d until_date:int = account.ResetPasswordResult; account.resetPasswordOk#e926d63e = account.ResetPasswordResult; +chatTheme#ed0b5c33 emoticon:string theme:Theme dark_theme:Theme = ChatTheme; + +account.chatThemesNotModified#e011e1c4 = account.ChatThemes; +account.chatThemes#fe4cbebd hash:int themes:Vector = account.ChatThemes; + +sponsoredMessage#f671f0d1 flags:# random_id:bytes peer_id:Peer from_id:Peer message:string media:flags.0?MessageMedia entities:flags.1?Vector = SponsoredMessage; + +messages.sponsoredMessages#65a4c7d5 messages:Vector chats:Vector users:Vector = messages.SponsoredMessages; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1365,6 +1376,7 @@ account.setGlobalPrivacySettings#1edaaac2 settings:GlobalPrivacySettings = Globa account.reportProfilePhoto#fa8cc6f5 peer:InputPeer photo_id:InputPhoto reason:ReportReason message:string = Bool; account.resetPassword#9308ce1b = account.ResetPasswordResult; account.declinePasswordReset#4c9409f6 = Bool; +account.getChatThemes#d6d71d7b hash:int = account.ChatThemes; users.getUsers#d91a548 id:Vector = Vector; users.getFullUser#ca30a5b1 id:InputUser = UserFull; @@ -1402,7 +1414,7 @@ messages.receivedMessages#5a954c0 max_id:int = Vector; messages.setTyping#58943ee2 flags:# peer:InputPeer top_msg_id:flags.0?int action:SendMessageAction = Bool; messages.sendMessage#520c3870 flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int = Updates; messages.sendMedia#3491eba9 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int = Updates; -messages.forwardMessages#d9fee60e flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true from_peer:InputPeer id:Vector random_id:Vector to_peer:InputPeer schedule_date:flags.10?int = Updates; +messages.forwardMessages#d9fee60e flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true from_peer:InputPeer id:Vector random_id:Vector to_peer:InputPeer schedule_date:flags.10?int = Updates; messages.reportSpam#cf1592db peer:InputPeer = Bool; messages.getPeerSettings#3672e09c peer:InputPeer = PeerSettings; messages.report#8953ab4e peer:InputPeer id:Vector reason:ReportReason message:string = Bool; @@ -1533,6 +1545,7 @@ messages.getAdminsWithInvites#3920e6ef peer:InputPeer = messages.ChatAdminsWithI messages.getChatInviteImporters#26fb7289 peer:InputPeer link:string offset_date:int offset_user:InputUser limit:int = messages.ChatInviteImporters; messages.setHistoryTTL#b80e5fe4 peer:InputPeer period:int = Updates; messages.checkHistoryImportPeer#5dc60f03 peer:InputPeer = messages.CheckedHistoryImportPeer; +messages.setChatTheme#e63be13f peer:InputPeer emoticon:string = Updates; updates.getState#edd4882a = updates.State; updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference; @@ -1611,6 +1624,8 @@ channels.editLocation#58e63f6d channel:InputChannel geo_point:InputGeoPoint addr channels.toggleSlowMode#edd49ef0 channel:InputChannel seconds:int = Updates; channels.getInactiveChannels#11e831ee = messages.InactiveChats; channels.convertToGigagroup#b290c69 channel:InputChannel = Updates; +channels.viewSponsoredMessage#beaedb94 channel:InputChannel random_id:bytes = Bool; +channels.getSponsoredMessages#ec210fbf channel:InputChannel = messages.SponsoredMessages; bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON; bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool; @@ -1649,10 +1664,10 @@ phone.leaveGroupCall#500377f9 call:InputGroupCall source:int = Updates; phone.inviteToGroupCall#7b393160 call:InputGroupCall users:Vector = Updates; phone.discardGroupCall#7a777135 call:InputGroupCall = Updates; phone.toggleGroupCallSettings#74bbb43d flags:# reset_invite_hash:flags.1?true call:InputGroupCall join_muted:flags.0?Bool = Updates; -phone.getGroupCall#c7cb017 call:InputGroupCall = phone.GroupCall; +phone.getGroupCall#41845db call:InputGroupCall limit:int = phone.GroupCall; phone.getGroupParticipants#c558d8ab call:InputGroupCall ids:Vector sources:Vector offset:string limit:int = phone.GroupParticipants; phone.checkGroupCall#b59cf977 call:InputGroupCall sources:Vector = Vector; -phone.toggleGroupCallRecord#c02a66d7 flags:# start:flags.0?true call:InputGroupCall title:flags.1?string = Updates; +phone.toggleGroupCallRecord#f128c708 flags:# start:flags.0?true video:flags.2?true call:InputGroupCall title:flags.1?string video_portrait:flags.2?Bool = Updates; phone.editGroupCallParticipant#a5273abf flags:# call:InputGroupCall participant:InputPeer muted:flags.0?Bool volume:flags.1?int raise_hand:flags.2?Bool video_stopped:flags.3?Bool video_paused:flags.4?Bool presentation_paused:flags.5?Bool = Updates; phone.editGroupCallTitle#1ca6ac0a call:InputGroupCall title:string = Updates; phone.getGroupCallJoinAs#ef7c213a peer:InputPeer = phone.JoinAsPeers; @@ -1678,4 +1693,4 @@ stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel stats.getMessagePublicForwards#5630281b channel:InputChannel msg_id:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats; -// LAYER 131 +// LAYER 132 From 391fbab67427e21514a35443acd6a0061d4f8d2b Mon Sep 17 00:00:00 2001 From: Devesh Pal Date: Wed, 1 Sep 2021 18:38:58 +0530 Subject: [PATCH 028/256] Implement Sticker Choosing Action (#3144) --- telethon/client/chats.py | 2 ++ telethon/events/userupdate.py | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/telethon/client/chats.py b/telethon/client/chats.py index ef7a10cb..dfbeddcc 100644 --- a/telethon/client/chats.py +++ b/telethon/client/chats.py @@ -22,6 +22,7 @@ class _ChatAction: 'contact': types.SendMessageChooseContactAction(), 'game': types.SendMessageGamePlayAction(), 'location': types.SendMessageGeoLocationAction(), + 'sticker': types.SendMessageChooseStickerAction(), 'record-audio': types.SendMessageRecordAudioAction(), 'record-voice': types.SendMessageRecordAudioAction(), # alias @@ -773,6 +774,7 @@ class ChatMethods: * ``'contact'``: choosing a contact. * ``'game'``: playing a game. * ``'location'``: choosing a geo location. + * ``'sticker'``: choosing a sticker. * ``'record-audio'``: recording a voice note. You may use ``'record-voice'`` as alias. * ``'record-round'``: recording a round video. diff --git a/telethon/events/userupdate.py b/telethon/events/userupdate.py index c0a07619..e0179f01 100644 --- a/telethon/events/userupdate.py +++ b/telethon/events/userupdate.py @@ -136,6 +136,7 @@ class UserUpdate(EventBuilder): """ return isinstance(self.action, ( types.SendMessageChooseContactAction, + types.SendMessageChooseStickerAction, types.SendMessageUploadAudioAction, types.SendMessageUploadDocumentAction, types.SendMessageUploadPhotoAction, @@ -228,6 +229,14 @@ class UserUpdate(EventBuilder): """ return isinstance(self.action, types.SendMessageUploadDocumentAction) + @property + @_requires_action + def sticker(self): + """ + `True` if what's being uploaded is a sticker. + """ + return isinstance(self.action, types.SendMessageChooseStickerAction) + @property @_requires_action def photo(self): From 28d3d4b122039e96c855be69d05edd644fb74a37 Mon Sep 17 00:00:00 2001 From: Shrimadhav U K Date: Sat, 11 Sep 2021 14:24:11 +0530 Subject: [PATCH 029/256] Update to layer 133 (#3155) --- telethon_generator/data/api.tl | 339 ++++++++++++++-------------- telethon_generator/data/errors.csv | 1 + telethon_generator/data/methods.csv | 1 + 3 files changed, 174 insertions(+), 167 deletions(-) diff --git a/telethon_generator/data/api.tl b/telethon_generator/data/api.tl index 82e59c8b..d9b3d1d7 100644 --- a/telethon_generator/data/api.tl +++ b/telethon_generator/data/api.tl @@ -41,16 +41,16 @@ null#56730bcc = Null; inputPeerEmpty#7f3b18ea = InputPeer; inputPeerSelf#7da07ec9 = InputPeer; -inputPeerChat#179be863 chat_id:int = InputPeer; -inputPeerUser#7b8e7de6 user_id:int access_hash:long = InputPeer; -inputPeerChannel#20adaef8 channel_id:int access_hash:long = InputPeer; -inputPeerUserFromMessage#17bae2e6 peer:InputPeer msg_id:int user_id:int = InputPeer; -inputPeerChannelFromMessage#9c95f7bb peer:InputPeer msg_id:int channel_id:int = InputPeer; +inputPeerChat#35a95cb9 chat_id:long = InputPeer; +inputPeerUser#dde8a54c user_id:long access_hash:long = InputPeer; +inputPeerChannel#27bcbbfc channel_id:long access_hash:long = InputPeer; +inputPeerUserFromMessage#a87b0a1c peer:InputPeer msg_id:int user_id:long = InputPeer; +inputPeerChannelFromMessage#bd2a0840 peer:InputPeer msg_id:int channel_id:long = InputPeer; inputUserEmpty#b98886cf = InputUser; inputUserSelf#f7c1b13f = InputUser; -inputUser#d8292816 user_id:int access_hash:long = InputUser; -inputUserFromMessage#2d117597 peer:InputPeer msg_id:int user_id:int = InputUser; +inputUser#f21158c6 user_id:long access_hash:long = InputUser; +inputUserFromMessage#1da448e2 peer:InputPeer msg_id:int user_id:long = InputUser; inputPhoneContact#f392b7f4 client_id:long phone:string first_name:string last_name:string = InputContact; @@ -94,9 +94,9 @@ inputPeerPhotoFileLocation#37257e99 flags:# big:flags.0?true peer:InputPeer phot inputStickerSetThumb#9d84f3db stickerset:InputStickerSet thumb_version:int = InputFileLocation; inputGroupCallStream#598a92a flags:# call:InputGroupCall time_ms:long scale:int video_channel:flags.0?int video_quality:flags.0?int = InputFileLocation; -peerUser#9db1bc6d user_id:int = Peer; -peerChat#bad0e5bb chat_id:int = Peer; -peerChannel#bddde532 channel_id:int = Peer; +peerUser#59511722 user_id:long = Peer; +peerChat#36c6019a chat_id:long = Peer; +peerChannel#a2a5371e channel_id:long = Peer; storage.fileUnknown#aa963b05 = storage.FileType; storage.filePartial#40bc6f52 = storage.FileType; @@ -109,8 +109,8 @@ storage.fileMov#4b09ebbc = storage.FileType; storage.fileMp4#b3cea0e4 = storage.FileType; storage.fileWebp#1081464c = storage.FileType; -userEmpty#200250ba id:int = User; -user#938458c1 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true id:int access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string = User; +userEmpty#d3bc4b7a id:long = User; +user#3ff6ecb0 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true id:long access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string = User; userProfilePhotoEmpty#4f11bae1 = UserProfilePhoto; userProfilePhoto#82d1f706 flags:# has_video:flags.0?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = UserProfilePhoto; @@ -122,33 +122,33 @@ userStatusRecently#e26f42f1 = UserStatus; userStatusLastWeek#7bf09fc = UserStatus; userStatusLastMonth#77ebc742 = UserStatus; -chatEmpty#9ba2d800 id:int = Chat; -chat#3bda1bde flags:# creator:flags.0?true kicked:flags.1?true left:flags.2?true deactivated:flags.5?true call_active:flags.23?true call_not_empty:flags.24?true id:int title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel admin_rights:flags.14?ChatAdminRights default_banned_rights:flags.18?ChatBannedRights = Chat; -chatForbidden#7328bdb id:int title:string = Chat; -channel#d31a961e flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true fake:flags.25?true gigagroup:flags.26?true id:int access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int version:int restriction_reason:flags.9?Vector admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int = Chat; -channelForbidden#289da732 flags:# broadcast:flags.5?true megagroup:flags.8?true id:int access_hash:long title:string until_date:flags.16?int = Chat; +chatEmpty#29562865 id:long = Chat; +chat#41cbf256 flags:# creator:flags.0?true kicked:flags.1?true left:flags.2?true deactivated:flags.5?true call_active:flags.23?true call_not_empty:flags.24?true id:long title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel admin_rights:flags.14?ChatAdminRights default_banned_rights:flags.18?ChatBannedRights = Chat; +chatForbidden#6592a1a7 id:long title:string = Chat; +channel#8261ac61 flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true fake:flags.25?true gigagroup:flags.26?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int restriction_reason:flags.9?Vector admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int = Chat; +channelForbidden#17d493d5 flags:# broadcast:flags.5?true megagroup:flags.8?true id:long access_hash:long title:string until_date:flags.16?int = Chat; -chatFull#49a0a5d9 flags:# can_set_username:flags.7?true has_scheduled:flags.8?true id:int about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer theme_emoticon:flags.16?string = ChatFull; -channelFull#2f532f3c flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?int location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string = ChatFull; +chatFull#4dbdc099 flags:# can_set_username:flags.7?true has_scheduled:flags.8?true id:long about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer theme_emoticon:flags.16?string = ChatFull; +channelFull#e9b27a17 flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string = ChatFull; -chatParticipant#c8d7493e user_id:int inviter_id:int date:int = ChatParticipant; -chatParticipantCreator#da13538a user_id:int = ChatParticipant; -chatParticipantAdmin#e2d6e436 user_id:int inviter_id:int date:int = ChatParticipant; +chatParticipant#c02d4007 user_id:long inviter_id:long date:int = ChatParticipant; +chatParticipantCreator#e46bcee4 user_id:long = ChatParticipant; +chatParticipantAdmin#a0933f5b user_id:long inviter_id:long date:int = ChatParticipant; -chatParticipantsForbidden#fc900c2b flags:# chat_id:int self_participant:flags.0?ChatParticipant = ChatParticipants; -chatParticipants#3f460fed chat_id:int participants:Vector version:int = ChatParticipants; +chatParticipantsForbidden#8763d3e1 flags:# chat_id:long self_participant:flags.0?ChatParticipant = ChatParticipants; +chatParticipants#3cbc93f8 chat_id:long participants:Vector version:int = ChatParticipants; chatPhotoEmpty#37c1011c = ChatPhoto; chatPhoto#1c6e1c11 flags:# has_video:flags.0?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = ChatPhoto; messageEmpty#90a6ca84 flags:# id:int peer_id:flags.0?Peer = Message; -message#bce383d2 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true id:int from_id:flags.8?Peer peer_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long restriction_reason:flags.22?Vector ttl_period:flags.25?int = Message; +message#85d6cbe2 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true id:int from_id:flags.8?Peer peer_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long restriction_reason:flags.22?Vector ttl_period:flags.25?int = Message; messageService#2b085862 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction ttl_period:flags.25?int = Message; messageMediaEmpty#3ded6320 = MessageMedia; messageMediaPhoto#695150d7 flags:# photo:flags.0?Photo ttl_seconds:flags.2?int = MessageMedia; messageMediaGeo#56e0d474 geo:GeoPoint = MessageMedia; -messageMediaContact#cbf24940 phone_number:string first_name:string last_name:string vcard:string user_id:int = MessageMedia; +messageMediaContact#70322949 phone_number:string first_name:string last_name:string vcard:string user_id:long = MessageMedia; messageMediaUnsupported#9f84f49e = MessageMedia; messageMediaDocument#9cb070d7 flags:# document:flags.0?Document ttl_seconds:flags.2?int = MessageMedia; messageMediaWebPage#a32dd600 webpage:WebPage = MessageMedia; @@ -160,16 +160,16 @@ messageMediaPoll#4bd6e798 poll:Poll results:PollResults = MessageMedia; messageMediaDice#3f7ee58b value:int emoticon:string = MessageMedia; messageActionEmpty#b6aef7b0 = MessageAction; -messageActionChatCreate#a6638b9a title:string users:Vector = MessageAction; +messageActionChatCreate#bd47cbad title:string users:Vector = MessageAction; messageActionChatEditTitle#b5a1ce5a title:string = MessageAction; messageActionChatEditPhoto#7fcb13a8 photo:Photo = MessageAction; messageActionChatDeletePhoto#95e3fbef = MessageAction; -messageActionChatAddUser#488a7337 users:Vector = MessageAction; -messageActionChatDeleteUser#b2ae9b0c user_id:int = MessageAction; -messageActionChatJoinedByLink#f89cf5e8 inviter_id:int = MessageAction; +messageActionChatAddUser#15cefd00 users:Vector = MessageAction; +messageActionChatDeleteUser#a43f30cc user_id:long = MessageAction; +messageActionChatJoinedByLink#31224c3 inviter_id:long = MessageAction; messageActionChannelCreate#95d2ac92 title:string = MessageAction; -messageActionChatMigrateTo#51bdb021 channel_id:int = MessageAction; -messageActionChannelMigrateFrom#b055eaee title:string chat_id:int = MessageAction; +messageActionChatMigrateTo#e1037f92 channel_id:long = MessageAction; +messageActionChannelMigrateFrom#ea3948e9 title:string chat_id:long = MessageAction; messageActionPinMessage#94bd38ed = MessageAction; messageActionHistoryClear#9fbab604 = MessageAction; messageActionGameScore#92a72876 game_id:long score:int = MessageAction; @@ -184,7 +184,7 @@ messageActionSecureValuesSent#d95c6154 types:Vector = MessageAc messageActionContactSignUp#f3f25f76 = MessageAction; messageActionGeoProximityReached#98e0d697 from_id:Peer to_id:Peer distance:int = MessageAction; messageActionGroupCall#7a0d7f42 flags:# call:InputGroupCall duration:flags.0?int = MessageAction; -messageActionInviteToGroupCall#76b9f11a call:InputGroupCall users:Vector = MessageAction; +messageActionInviteToGroupCall#502f92f7 call:InputGroupCall users:Vector = MessageAction; messageActionSetMessagesTTL#aa1afbfd period:int = MessageAction; messageActionGroupCallScheduled#b3a07661 call:InputGroupCall schedule_date:int = MessageAction; messageActionSetChatTheme#aa786345 emoticon:string = MessageAction; @@ -210,7 +210,7 @@ auth.sentCode#5e002502 flags:# type:auth.SentCodeType phone_code_hash:string nex auth.authorization#cd050916 flags:# tmp_sessions:flags.0?int user:User = auth.Authorization; auth.authorizationSignUpRequired#44747e9a flags:# terms_of_service:flags.0?help.TermsOfService = auth.Authorization; -auth.exportedAuthorization#df969c2d id:int bytes:bytes = auth.ExportedAuthorization; +auth.exportedAuthorization#b434e2b8 id:long bytes:bytes = auth.ExportedAuthorization; inputNotifyPeer#b8bc5b0c peer:InputPeer = InputNotifyPeer; inputNotifyUsers#193b4417 = InputNotifyPeer; @@ -237,11 +237,11 @@ inputReportReasonFake#f5ddd6e7 = ReportReason; userFull#d697ff05 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true user:User about:flags.1?string settings:PeerSettings profile_photo:flags.2?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string = UserFull; -contact#f911c994 user_id:int mutual:Bool = Contact; +contact#145ade0b user_id:long mutual:Bool = Contact; -importedContact#d0028438 user_id:int client_id:long = ImportedContact; +importedContact#c13e3c50 user_id:long client_id:long = ImportedContact; -contactStatus#d3680c61 user_id:int status:UserStatus = ContactStatus; +contactStatus#16d9703b user_id:long status:UserStatus = ContactStatus; contacts.contactsNotModified#b74ba9d2 = contacts.Contacts; contacts.contacts#eae87e42 contacts:Vector saved_count:int users:Vector = contacts.Contacts; @@ -288,64 +288,64 @@ inputMessagesFilterPinned#1bb00451 = MessagesFilter; updateNewMessage#1f2b0afd message:Message pts:int pts_count:int = Update; updateMessageID#4e90bfd6 id:int random_id:long = Update; updateDeleteMessages#a20db0e5 messages:Vector pts:int pts_count:int = Update; -updateUserTyping#5c486927 user_id:int action:SendMessageAction = Update; -updateChatUserTyping#86cadb6c chat_id:int from_id:Peer action:SendMessageAction = Update; +updateUserTyping#c01e857f user_id:long action:SendMessageAction = Update; +updateChatUserTyping#83487af0 chat_id:long from_id:Peer action:SendMessageAction = Update; updateChatParticipants#7761198 participants:ChatParticipants = Update; -updateUserStatus#1bfbd823 user_id:int status:UserStatus = Update; -updateUserName#a7332b73 user_id:int first_name:string last_name:string username:string = Update; -updateUserPhoto#95313b0c user_id:int date:int photo:UserProfilePhoto previous:Bool = Update; +updateUserStatus#e5bdf8de user_id:long status:UserStatus = Update; +updateUserName#c3f202e0 user_id:long first_name:string last_name:string username:string = Update; +updateUserPhoto#f227868c user_id:long date:int photo:UserProfilePhoto previous:Bool = Update; updateNewEncryptedMessage#12bcbd9a message:EncryptedMessage qts:int = Update; updateEncryptedChatTyping#1710f156 chat_id:int = Update; updateEncryption#b4a2e88d chat:EncryptedChat date:int = Update; updateEncryptedMessagesRead#38fe25b7 chat_id:int max_date:int date:int = Update; -updateChatParticipantAdd#ea4b0e5c chat_id:int user_id:int inviter_id:int date:int version:int = Update; -updateChatParticipantDelete#6e5f8c22 chat_id:int user_id:int version:int = Update; +updateChatParticipantAdd#3dda5451 chat_id:long user_id:long inviter_id:long date:int version:int = Update; +updateChatParticipantDelete#e32f3d77 chat_id:long user_id:long version:int = Update; updateDcOptions#8e5e9873 dc_options:Vector = Update; updateNotifySettings#bec268ef peer:NotifyPeer notify_settings:PeerNotifySettings = Update; updateServiceNotification#ebe46819 flags:# popup:flags.0?true inbox_date:flags.1?int type:string message:string media:MessageMedia entities:Vector = Update; updatePrivacy#ee3b272a key:PrivacyKey rules:Vector = Update; -updateUserPhone#12b9417b user_id:int phone:string = Update; +updateUserPhone#5492a13 user_id:long phone:string = Update; updateReadHistoryInbox#9c974fdf flags:# folder_id:flags.0?int peer:Peer max_id:int still_unread_count:int pts:int pts_count:int = Update; updateReadHistoryOutbox#2f2f21bf peer:Peer max_id:int pts:int pts_count:int = Update; updateWebPage#7f891213 webpage:WebPage pts:int pts_count:int = Update; updateReadMessagesContents#68c13933 messages:Vector pts:int pts_count:int = Update; -updateChannelTooLong#eb0467fb flags:# channel_id:int pts:flags.0?int = Update; -updateChannel#b6d45656 channel_id:int = Update; +updateChannelTooLong#108d941f flags:# channel_id:long pts:flags.0?int = Update; +updateChannel#635b4c09 channel_id:long = Update; updateNewChannelMessage#62ba04d9 message:Message pts:int pts_count:int = Update; -updateReadChannelInbox#330b5424 flags:# folder_id:flags.0?int channel_id:int max_id:int still_unread_count:int pts:int = Update; -updateDeleteChannelMessages#c37521c9 channel_id:int messages:Vector pts:int pts_count:int = Update; -updateChannelMessageViews#98a12b4b channel_id:int id:int views:int = Update; -updateChatParticipantAdmin#b6901959 chat_id:int user_id:int is_admin:Bool version:int = Update; +updateReadChannelInbox#922e6e10 flags:# folder_id:flags.0?int channel_id:long max_id:int still_unread_count:int pts:int = Update; +updateDeleteChannelMessages#c32d5b12 channel_id:long messages:Vector pts:int pts_count:int = Update; +updateChannelMessageViews#f226ac08 channel_id:long id:int views:int = Update; +updateChatParticipantAdmin#d7ca61a2 chat_id:long user_id:long is_admin:Bool version:int = Update; updateNewStickerSet#688a30aa stickerset:messages.StickerSet = Update; updateStickerSetsOrder#bb2d201 flags:# masks:flags.0?true order:Vector = Update; updateStickerSets#43ae3dec = Update; updateSavedGifs#9375341e = Update; -updateBotInlineQuery#3f2038db flags:# query_id:long user_id:int query:string geo:flags.0?GeoPoint peer_type:flags.1?InlineQueryPeerType offset:string = Update; -updateBotInlineSend#e48f964 flags:# user_id:int query:string geo:flags.0?GeoPoint id:string msg_id:flags.1?InputBotInlineMessageID = Update; +updateBotInlineQuery#496f379c flags:# query_id:long user_id:long query:string geo:flags.0?GeoPoint peer_type:flags.1?InlineQueryPeerType offset:string = Update; +updateBotInlineSend#12f12a07 flags:# user_id:long query:string geo:flags.0?GeoPoint id:string msg_id:flags.1?InputBotInlineMessageID = Update; updateEditChannelMessage#1b3f4df7 message:Message pts:int pts_count:int = Update; -updateBotCallbackQuery#e73547e1 flags:# query_id:long user_id:int peer:Peer msg_id:int chat_instance:long data:flags.0?bytes game_short_name:flags.1?string = Update; +updateBotCallbackQuery#b9cfc48d flags:# query_id:long user_id:long peer:Peer msg_id:int chat_instance:long data:flags.0?bytes game_short_name:flags.1?string = Update; updateEditMessage#e40370a3 message:Message pts:int pts_count:int = Update; -updateInlineBotCallbackQuery#f9d27a5a flags:# query_id:long user_id:int msg_id:InputBotInlineMessageID chat_instance:long data:flags.0?bytes game_short_name:flags.1?string = Update; -updateReadChannelOutbox#25d6c9c7 channel_id:int max_id:int = Update; +updateInlineBotCallbackQuery#691e9052 flags:# query_id:long user_id:long msg_id:InputBotInlineMessageID chat_instance:long data:flags.0?bytes game_short_name:flags.1?string = Update; +updateReadChannelOutbox#b75f99a9 channel_id:long max_id:int = Update; updateDraftMessage#ee2bb969 peer:Peer draft:DraftMessage = Update; updateReadFeaturedStickers#571d2742 = Update; updateRecentStickers#9a422c20 = Update; updateConfig#a229dd06 = Update; updatePtsChanged#3354678f = Update; -updateChannelWebPage#40771900 channel_id:int webpage:WebPage pts:int pts_count:int = Update; +updateChannelWebPage#2f2ba99f channel_id:long webpage:WebPage pts:int pts_count:int = Update; updateDialogPinned#6e6fe51c flags:# pinned:flags.0?true folder_id:flags.1?int peer:DialogPeer = Update; updatePinnedDialogs#fa0f3ca2 flags:# folder_id:flags.1?int order:flags.0?Vector = Update; updateBotWebhookJSON#8317c0c3 data:DataJSON = Update; updateBotWebhookJSONQuery#9b9240a6 query_id:long data:DataJSON timeout:int = Update; -updateBotShippingQuery#e0cdc940 query_id:long user_id:int payload:bytes shipping_address:PostAddress = Update; -updateBotPrecheckoutQuery#5d2f3aa9 flags:# query_id:long user_id:int payload:bytes info:flags.0?PaymentRequestedInfo shipping_option_id:flags.1?string currency:string total_amount:long = Update; +updateBotShippingQuery#b5aefd7d query_id:long user_id:long payload:bytes shipping_address:PostAddress = Update; +updateBotPrecheckoutQuery#8caa9a96 flags:# query_id:long user_id:long payload:bytes info:flags.0?PaymentRequestedInfo shipping_option_id:flags.1?string currency:string total_amount:long = Update; updatePhoneCall#ab0f6b1e phone_call:PhoneCall = Update; updateLangPackTooLong#46560264 lang_code:string = Update; updateLangPack#56022f4d difference:LangPackDifference = Update; updateFavedStickers#e511996d = Update; -updateChannelReadMessagesContents#89893b45 channel_id:int messages:Vector = Update; +updateChannelReadMessagesContents#44bdd535 channel_id:long messages:Vector = Update; updateContactsReset#7084a7be = Update; -updateChannelAvailableMessages#70db6837 channel_id:int available_min_id:int = Update; +updateChannelAvailableMessages#b23fc698 channel_id:long available_min_id:int = Update; updateDialogUnreadMark#e16459c3 flags:# unread:flags.0?true peer:DialogPeer = Update; updateMessagePoll#aca1657b flags:# poll_id:long poll:flags.0?Poll results:PollResults = Update; updateChatDefaultBannedRights#54c01850 peer:Peer default_banned_rights:ChatBannedRights version:int = Update; @@ -357,27 +357,27 @@ updateDeleteScheduledMessages#90866cee peer:Peer messages:Vector = Update; updateTheme#8216fba3 theme:Theme = Update; updateGeoLiveViewed#871fb939 peer:Peer msg_id:int = Update; updateLoginToken#564fe691 = Update; -updateMessagePollVote#37f69f0b poll_id:long user_id:int options:Vector qts:int = Update; +updateMessagePollVote#106395c9 poll_id:long user_id:long options:Vector qts:int = Update; updateDialogFilter#26ffde7d flags:# id:int filter:flags.0?DialogFilter = Update; updateDialogFilterOrder#a5d72105 order:Vector = Update; updateDialogFilters#3504914f = Update; updatePhoneCallSignalingData#2661bf09 phone_call_id:long data:bytes = Update; -updateChannelMessageForwards#6e8a84df channel_id:int id:int forwards:int = Update; -updateReadChannelDiscussionInbox#1cc7de54 flags:# channel_id:int top_msg_id:int read_max_id:int broadcast_id:flags.0?int broadcast_post:flags.0?int = Update; -updateReadChannelDiscussionOutbox#4638a26c channel_id:int top_msg_id:int read_max_id:int = Update; +updateChannelMessageForwards#d29a27f4 channel_id:long id:int forwards:int = Update; +updateReadChannelDiscussionInbox#d6b19546 flags:# channel_id:long top_msg_id:int read_max_id:int broadcast_id:flags.0?long broadcast_post:flags.0?int = Update; +updateReadChannelDiscussionOutbox#695c9e7c channel_id:long top_msg_id:int read_max_id:int = Update; updatePeerBlocked#246a4b22 peer_id:Peer blocked:Bool = Update; -updateChannelUserTyping#6b171718 flags:# channel_id:int top_msg_id:flags.0?int from_id:Peer action:SendMessageAction = Update; +updateChannelUserTyping#8c88c923 flags:# channel_id:long top_msg_id:flags.0?int from_id:Peer action:SendMessageAction = Update; updatePinnedMessages#ed85eab5 flags:# pinned:flags.0?true peer:Peer messages:Vector pts:int pts_count:int = Update; -updatePinnedChannelMessages#8588878b flags:# pinned:flags.0?true channel_id:int messages:Vector pts:int pts_count:int = Update; -updateChat#1330a196 chat_id:int = Update; +updatePinnedChannelMessages#5bb98608 flags:# pinned:flags.0?true channel_id:long messages:Vector pts:int pts_count:int = Update; +updateChat#f89a6a4e chat_id:long = Update; updateGroupCallParticipants#f2ebdb4e call:InputGroupCall participants:Vector version:int = Update; -updateGroupCall#a45eb99b chat_id:int call:GroupCall = Update; +updateGroupCall#14b24500 chat_id:long call:GroupCall = Update; updatePeerHistoryTTL#bb9bb9a5 flags:# peer:Peer ttl_period:flags.0?int = Update; -updateChatParticipant#f3b3781f flags:# chat_id:int date:int actor_id:int user_id:int prev_participant:flags.0?ChatParticipant new_participant:flags.1?ChatParticipant invite:flags.2?ExportedChatInvite qts:int = Update; -updateChannelParticipant#7fecb1ec flags:# channel_id:int date:int actor_id:int user_id:int prev_participant:flags.0?ChannelParticipant new_participant:flags.1?ChannelParticipant invite:flags.2?ExportedChatInvite qts:int = Update; -updateBotStopped#7f9488a user_id:int date:int stopped:Bool qts:int = Update; +updateChatParticipant#d087663a flags:# chat_id:long date:int actor_id:long user_id:long prev_participant:flags.0?ChatParticipant new_participant:flags.1?ChatParticipant invite:flags.2?ExportedChatInvite qts:int = Update; +updateChannelParticipant#985d3abb flags:# channel_id:long date:int actor_id:long user_id:long prev_participant:flags.0?ChannelParticipant new_participant:flags.1?ChannelParticipant invite:flags.2?ExportedChatInvite qts:int = Update; +updateBotStopped#c4870a49 user_id:long date:int stopped:Bool qts:int = Update; updateGroupCallConnection#b783982 flags:# presentation:flags.0?true params:DataJSON = Update; -updateBotCommands#cf7e0873 peer:Peer bot_id:int commands:Vector = Update; +updateBotCommands#4d712f2e peer:Peer bot_id:long commands:Vector = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -387,8 +387,8 @@ updates.differenceSlice#a8fb1981 new_messages:Vector new_encrypted_mess updates.differenceTooLong#4afe8f6d pts:int = updates.Difference; updatesTooLong#e317af7e = Updates; -updateShortMessage#faeff833 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true id:int user_id:int message:string pts:int pts_count:int date:int fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to:flags.3?MessageReplyHeader entities:flags.7?Vector ttl_period:flags.25?int = Updates; -updateShortChatMessage#1157b858 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true id:int from_id:int chat_id:int message:string pts:int pts_count:int date:int fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to:flags.3?MessageReplyHeader entities:flags.7?Vector ttl_period:flags.25?int = Updates; +updateShortMessage#313bc7f8 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true id:int user_id:long message:string pts:int pts_count:int date:int fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long reply_to:flags.3?MessageReplyHeader entities:flags.7?Vector ttl_period:flags.25?int = Updates; +updateShortChatMessage#4d6deea5 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true id:int from_id:long chat_id:long message:string pts:int pts_count:int date:int fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long reply_to:flags.3?MessageReplyHeader entities:flags.7?Vector ttl_period:flags.25?int = Updates; updateShort#78d4dec1 update:Update date:int = Updates; updatesCombined#725b04c3 updates:Vector users:Vector chats:Vector date:int seq_start:int seq:int = Updates; updates#74ae4240 updates:Vector users:Vector chats:Vector date:int seq:int = Updates; @@ -414,9 +414,9 @@ help.noAppUpdate#c45a6536 = help.AppUpdate; help.inviteText#18cb9f78 message:string = help.InviteText; encryptedChatEmpty#ab7ec0a0 id:int = EncryptedChat; -encryptedChatWaiting#3bf703dc id:int access_hash:long date:int admin_id:int participant_id:int = EncryptedChat; -encryptedChatRequested#62718a82 flags:# folder_id:flags.0?int id:int access_hash:long date:int admin_id:int participant_id:int g_a:bytes = EncryptedChat; -encryptedChat#fa56ce36 id:int access_hash:long date:int admin_id:int participant_id:int g_a_or_b:bytes key_fingerprint:long = EncryptedChat; +encryptedChatWaiting#66b25953 id:int access_hash:long date:int admin_id:long participant_id:long = EncryptedChat; +encryptedChatRequested#48f1d94c flags:# folder_id:flags.0?int id:int access_hash:long date:int admin_id:long participant_id:long g_a:bytes = EncryptedChat; +encryptedChat#61f0d4c7 id:int access_hash:long date:int admin_id:long participant_id:long g_a_or_b:bytes key_fingerprint:long = EncryptedChat; encryptedChatDiscarded#1e1c7c45 flags:# history_deleted:flags.0?true id:int = EncryptedChat; inputEncryptedChat#f141b5e1 chat_id:int access_hash:long = InputEncryptedChat; @@ -467,6 +467,8 @@ sendMessageUploadRoundAction#243e1c66 progress:int = SendMessageAction; speakingInGroupCallAction#d92c2285 = SendMessageAction; sendMessageHistoryImportAction#dbda9246 progress:int = SendMessageAction; sendMessageChooseStickerAction#b05ac6b1 = SendMessageAction; +sendMessageEmojiInteraction#6a3233b6 emoticon:string interaction:DataJSON = SendMessageAction; +sendMessageEmojiInteractionSeen#b665902e emoticon:string = SendMessageAction; contacts.found#b3134d9d my_results:Vector results:Vector chats:Vector users:Vector = contacts.Found; @@ -494,17 +496,17 @@ inputPrivacyValueAllowUsers#131cc67f users:Vector = InputPrivacyRule; inputPrivacyValueDisallowContacts#ba52007 = InputPrivacyRule; inputPrivacyValueDisallowAll#d66b66c9 = InputPrivacyRule; inputPrivacyValueDisallowUsers#90110467 users:Vector = InputPrivacyRule; -inputPrivacyValueAllowChatParticipants#4c81c1ba chats:Vector = InputPrivacyRule; -inputPrivacyValueDisallowChatParticipants#d82363af chats:Vector = InputPrivacyRule; +inputPrivacyValueAllowChatParticipants#840649cf chats:Vector = InputPrivacyRule; +inputPrivacyValueDisallowChatParticipants#e94f0f86 chats:Vector = InputPrivacyRule; privacyValueAllowContacts#fffe1bac = PrivacyRule; privacyValueAllowAll#65427b82 = PrivacyRule; -privacyValueAllowUsers#4d5bbe0c users:Vector = PrivacyRule; +privacyValueAllowUsers#b8905fb2 users:Vector = PrivacyRule; privacyValueDisallowContacts#f888fa1a = PrivacyRule; privacyValueDisallowAll#8b73e763 = PrivacyRule; -privacyValueDisallowUsers#c7f49b7 users:Vector = PrivacyRule; -privacyValueAllowChatParticipants#18be796b chats:Vector = PrivacyRule; -privacyValueDisallowChatParticipants#acae0690 chats:Vector = PrivacyRule; +privacyValueDisallowUsers#e4621141 users:Vector = PrivacyRule; +privacyValueAllowChatParticipants#6b134e8e chats:Vector = PrivacyRule; +privacyValueDisallowChatParticipants#41c87565 chats:Vector = PrivacyRule; account.privacyRules#50a04e45 rules:Vector chats:Vector users:Vector = account.PrivacyRules; @@ -519,12 +521,12 @@ documentAttributeFilename#15590068 file_name:string = DocumentAttribute; documentAttributeHasStickers#9801d2f7 = DocumentAttribute; messages.stickersNotModified#f1749a22 = messages.Stickers; -messages.stickers#e4599bbd hash:int stickers:Vector = messages.Stickers; +messages.stickers#30a6ec7e hash:long stickers:Vector = messages.Stickers; stickerPack#12b299d4 emoticon:string documents:Vector = StickerPack; messages.allStickersNotModified#e86602c3 = messages.AllStickers; -messages.allStickers#edfd405f hash:int sets:Vector = messages.AllStickers; +messages.allStickers#cdbbcebb hash:long sets:Vector = messages.AllStickers; messages.affectedMessages#84d19185 pts:int pts_count:int = messages.AffectedMessages; @@ -547,7 +549,7 @@ auth.passwordRecovery#137948a5 email_pattern:string = auth.PasswordRecovery; receivedNotifyMessage#a384b779 id:int flags:int = ReceivedNotifyMessage; -chatInviteExported#6e24fc9d flags:# revoked:flags.0?true permanent:flags.5?true link:string admin_id:int date:int start_date:flags.4?int expire_date:flags.1?int usage_limit:flags.2?int usage:flags.3?int = ExportedChatInvite; +chatInviteExported#b18105e8 flags:# revoked:flags.0?true permanent:flags.5?true link:string admin_id:long date:int start_date:flags.4?int expire_date:flags.1?int usage_limit:flags.2?int usage:flags.3?int = ExportedChatInvite; chatInviteAlready#5a686d7c chat:Chat = ChatInvite; chatInvite#dfc2f58e flags:# channel:flags.0?true broadcast:flags.1?true public:flags.2?true megagroup:flags.3?true title:string photo:Photo participants_count:int participants:flags.4?Vector = ChatInvite; @@ -565,7 +567,7 @@ messages.stickerSet#b60a24a6 set:StickerSet packs:Vector documents: botCommand#c27ac8c7 command:string description:string = BotCommand; -botInfo#98e81d3a user_id:int description:string commands:Vector = BotInfo; +botInfo#1b74b335 user_id:long description:string commands:Vector = BotInfo; keyboardButton#a2fa4880 text:string = KeyboardButton; keyboardButtonUrl#258aff05 text:string url:string = KeyboardButton; @@ -597,7 +599,7 @@ messageEntityItalic#826f8b60 offset:int length:int = MessageEntity; messageEntityCode#28a20571 offset:int length:int = MessageEntity; messageEntityPre#73924be0 offset:int length:int language:string = MessageEntity; messageEntityTextUrl#76a6d327 offset:int length:int url:string = MessageEntity; -messageEntityMentionName#352dca58 offset:int length:int user_id:int = MessageEntity; +messageEntityMentionName#dc7b1140 offset:int length:int user_id:long = MessageEntity; inputMessageEntityMentionName#208e68c9 offset:int length:int user_id:InputUser = MessageEntity; messageEntityPhone#9b69e34b offset:int length:int = MessageEntity; messageEntityCashtag#4c4e743f offset:int length:int = MessageEntity; @@ -607,8 +609,8 @@ messageEntityBlockquote#20df5d0 offset:int length:int = MessageEntity; messageEntityBankCard#761e6af4 offset:int length:int = MessageEntity; inputChannelEmpty#ee8c1e86 = InputChannel; -inputChannel#afeb712e channel_id:int access_hash:long = InputChannel; -inputChannelFromMessage#2a286531 peer:InputPeer msg_id:int channel_id:int = InputChannel; +inputChannel#f35aec28 channel_id:long access_hash:long = InputChannel; +inputChannelFromMessage#5b934f9d peer:InputPeer msg_id:int channel_id:long = InputChannel; contacts.resolvedPeer#7f077ad9 peer:Peer chats:Vector users:Vector = contacts.ResolvedPeer; @@ -621,11 +623,11 @@ updates.channelDifference#2064674e flags:# final:flags.0?true pts:int timeout:fl channelMessagesFilterEmpty#94d42ee7 = ChannelMessagesFilter; channelMessagesFilter#cd77d957 flags:# exclude_new_messages:flags.1?true ranges:Vector = ChannelMessagesFilter; -channelParticipant#15ebac1d user_id:int date:int = ChannelParticipant; -channelParticipantSelf#a3289a6d user_id:int inviter_id:int date:int = ChannelParticipant; -channelParticipantCreator#447dca4b flags:# user_id:int admin_rights:ChatAdminRights rank:flags.0?string = ChannelParticipant; -channelParticipantAdmin#ccbebbaf flags:# can_edit:flags.0?true self:flags.1?true user_id:int inviter_id:flags.1?int promoted_by:int date:int admin_rights:ChatAdminRights rank:flags.2?string = ChannelParticipant; -channelParticipantBanned#50a1dfd6 flags:# left:flags.0?true peer:Peer kicked_by:int date:int banned_rights:ChatBannedRights = ChannelParticipant; +channelParticipant#c00c07c0 user_id:long date:int = ChannelParticipant; +channelParticipantSelf#28a8bc67 user_id:long inviter_id:long date:int = ChannelParticipant; +channelParticipantCreator#2fe601d3 flags:# user_id:long admin_rights:ChatAdminRights rank:flags.0?string = ChannelParticipant; +channelParticipantAdmin#34c3bb53 flags:# can_edit:flags.0?true self:flags.1?true user_id:long inviter_id:flags.1?long promoted_by:long date:int admin_rights:ChatAdminRights rank:flags.2?string = ChannelParticipant; +channelParticipantBanned#6df8014e flags:# left:flags.0?true peer:Peer kicked_by:long date:int banned_rights:ChatBannedRights = ChannelParticipant; channelParticipantLeft#1b03f006 peer:Peer = ChannelParticipant; channelParticipantsRecent#de3f3c79 = ChannelParticipantsFilter; @@ -645,7 +647,7 @@ channels.channelParticipant#dfb80317 participant:ChannelParticipant chats:Vector help.termsOfService#780a0310 flags:# popup:flags.0?true id:DataJSON text:string entities:Vector min_age_confirm:flags.1?int = help.TermsOfService; messages.savedGifsNotModified#e8025ca2 = messages.SavedGifs; -messages.savedGifs#2e0709a5 hash:int gifs:Vector = messages.SavedGifs; +messages.savedGifs#84a02a0d hash:long gifs:Vector = messages.SavedGifs; inputBotInlineMessageMediaAuto#3380c786 flags:# message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageText#3dcd7a87 flags:# no_webpage:flags.0?true message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; @@ -690,6 +692,7 @@ messages.botCallbackAnswer#36585ea4 flags:# alert:flags.1?true has_url:flags.3?t messages.messageEditData#26b5dde6 flags:# caption:flags.0?true = messages.MessageEditData; inputBotInlineMessageID#890c3d89 dc_id:int id:long access_hash:long = InputBotInlineMessageID; +inputBotInlineMessageID64#b6d915d7 dc_id:int owner_id:long id:int access_hash:long = InputBotInlineMessageID; inlineBotSwitchPM#3c20629f text:string start_param:string = InlineBotSwitchPM; @@ -716,10 +719,10 @@ draftMessageEmpty#1b0c841a flags:# date:flags.0?int = DraftMessage; draftMessage#fd8e711f flags:# no_webpage:flags.1?true reply_to_msg_id:flags.0?int message:string entities:flags.3?Vector date:int = DraftMessage; messages.featuredStickersNotModified#c6dc0c66 count:int = messages.FeaturedStickers; -messages.featuredStickers#b6abc341 hash:int count:int sets:Vector unread:Vector = messages.FeaturedStickers; +messages.featuredStickers#84c02310 hash:long count:int sets:Vector unread:Vector = messages.FeaturedStickers; messages.recentStickersNotModified#b17f890 = messages.RecentStickers; -messages.recentStickers#22f3afb3 hash:int packs:Vector stickers:Vector dates:Vector = messages.RecentStickers; +messages.recentStickers#88d37c56 hash:long packs:Vector stickers:Vector dates:Vector = messages.RecentStickers; messages.archivedStickers#4fcba9c8 count:int sets:Vector = messages.ArchivedStickers; @@ -739,7 +742,7 @@ game#bdf9653b flags:# id:long access_hash:long short_name:string title:string de inputGameID#32c3e77 id:long access_hash:long = InputGame; inputGameShortName#c331e80a bot_id:InputUser short_name:string = InputGame; -highScore#58fffcd0 pos:int user_id:int score:int = HighScore; +highScore#73a379eb pos:int user_id:long score:int = HighScore; messages.highScores#9a3bfd99 scores:Vector users:Vector = messages.HighScores; @@ -819,14 +822,14 @@ inputWebFileGeoPointLocation#9f2221c9 geo_point:InputGeoPoint access_hash:long w upload.webFile#21e753bc size:int mime_type:string file_type:storage.FileType mtime:int bytes:bytes = upload.WebFile; -payments.paymentForm#8d0b2415 flags:# can_save_credentials:flags.2?true password_missing:flags.3?true form_id:long bot_id:int invoice:Invoice provider_id:int url:string native_provider:flags.4?string native_params:flags.4?DataJSON saved_info:flags.0?PaymentRequestedInfo saved_credentials:flags.1?PaymentSavedCredentials users:Vector = payments.PaymentForm; +payments.paymentForm#1694761b flags:# can_save_credentials:flags.2?true password_missing:flags.3?true form_id:long bot_id:long invoice:Invoice provider_id:long url:string native_provider:flags.4?string native_params:flags.4?DataJSON saved_info:flags.0?PaymentRequestedInfo saved_credentials:flags.1?PaymentSavedCredentials users:Vector = payments.PaymentForm; payments.validatedRequestedInfo#d1451883 flags:# id:flags.0?string shipping_options:flags.1?Vector = payments.ValidatedRequestedInfo; payments.paymentResult#4e5f810d updates:Updates = payments.PaymentResult; payments.paymentVerificationNeeded#d8411139 url:string = payments.PaymentResult; -payments.paymentReceipt#10b555d0 flags:# date:int bot_id:int provider_id:int title:string description:string photo:flags.2?WebDocument invoice:Invoice info:flags.0?PaymentRequestedInfo shipping:flags.1?ShippingOption tip_amount:flags.3?long currency:string total_amount:long credentials_title:string users:Vector = payments.PaymentReceipt; +payments.paymentReceipt#70c4fe03 flags:# date:int bot_id:long provider_id:long title:string description:string photo:flags.2?WebDocument invoice:Invoice info:flags.0?PaymentRequestedInfo shipping:flags.1?ShippingOption tip_amount:flags.3?long currency:string total_amount:long credentials_title:string users:Vector = payments.PaymentReceipt; payments.savedInfo#fb8fe43c flags:# has_saved_credentials:flags.1?true saved_info:flags.0?PaymentRequestedInfo = payments.SavedInfo; @@ -844,10 +847,10 @@ inputStickerSetItem#ffa0a496 flags:# document:InputDocument emoji:string mask_co inputPhoneCall#1e36fded id:long access_hash:long = InputPhoneCall; phoneCallEmpty#5366c915 id:long = PhoneCall; -phoneCallWaiting#1b8f4ad1 flags:# video:flags.6?true id:long access_hash:long date:int admin_id:int participant_id:int protocol:PhoneCallProtocol receive_date:flags.0?int = PhoneCall; -phoneCallRequested#87eabb53 flags:# video:flags.6?true id:long access_hash:long date:int admin_id:int participant_id:int g_a_hash:bytes protocol:PhoneCallProtocol = PhoneCall; -phoneCallAccepted#997c454a flags:# video:flags.6?true id:long access_hash:long date:int admin_id:int participant_id:int g_b:bytes protocol:PhoneCallProtocol = PhoneCall; -phoneCall#8742ae7f flags:# p2p_allowed:flags.5?true video:flags.6?true id:long access_hash:long date:int admin_id:int participant_id:int g_a_or_b:bytes key_fingerprint:long protocol:PhoneCallProtocol connections:Vector start_date:int = PhoneCall; +phoneCallWaiting#c5226f17 flags:# video:flags.6?true id:long access_hash:long date:int admin_id:long participant_id:long protocol:PhoneCallProtocol receive_date:flags.0?int = PhoneCall; +phoneCallRequested#14b0ed0c flags:# video:flags.6?true id:long access_hash:long date:int admin_id:long participant_id:long g_a_hash:bytes protocol:PhoneCallProtocol = PhoneCall; +phoneCallAccepted#3660c311 flags:# video:flags.6?true id:long access_hash:long date:int admin_id:long participant_id:long g_b:bytes protocol:PhoneCallProtocol = PhoneCall; +phoneCall#967f7c67 flags:# p2p_allowed:flags.5?true video:flags.6?true id:long access_hash:long date:int admin_id:long participant_id:long g_a_or_b:bytes key_fingerprint:long protocol:PhoneCallProtocol connections:Vector start_date:int = PhoneCall; phoneCallDiscarded#50ca4de1 flags:# need_rating:flags.2?true need_debug:flags.3?true video:flags.6?true id:long reason:flags.0?PhoneCallDiscardReason duration:flags.1?int = PhoneCall; phoneConnection#9d4c17c0 id:long ip:string ipv6:string port:int peer_tag:bytes = PhoneConnection; @@ -890,7 +893,7 @@ channelAdminLogEventActionChangeStickerSet#b1c3caa7 prev_stickerset:InputSticker channelAdminLogEventActionTogglePreHistoryHidden#5f5c95f1 new_value:Bool = ChannelAdminLogEventAction; channelAdminLogEventActionDefaultBannedRights#2df5fc0a prev_banned_rights:ChatBannedRights new_banned_rights:ChatBannedRights = ChannelAdminLogEventAction; channelAdminLogEventActionStopPoll#8f079643 message:Message = ChannelAdminLogEventAction; -channelAdminLogEventActionChangeLinkedChat#a26f881b prev_value:int new_value:int = ChannelAdminLogEventAction; +channelAdminLogEventActionChangeLinkedChat#50c7ac8 prev_value:long new_value:long = ChannelAdminLogEventAction; channelAdminLogEventActionChangeLocation#e6b76ae prev_value:ChannelLocation new_value:ChannelLocation = ChannelAdminLogEventAction; channelAdminLogEventActionToggleSlowMode#53909779 prev_value:int new_value:int = ChannelAdminLogEventAction; channelAdminLogEventActionStartGroupCall#23209745 call:InputGroupCall = ChannelAdminLogEventAction; @@ -904,8 +907,9 @@ channelAdminLogEventActionExportedInviteRevoke#410a134e invite:ExportedChatInvit channelAdminLogEventActionExportedInviteEdit#e90ebb59 prev_invite:ExportedChatInvite new_invite:ExportedChatInvite = ChannelAdminLogEventAction; channelAdminLogEventActionParticipantVolume#3e7f6847 participant:GroupCallParticipant = ChannelAdminLogEventAction; channelAdminLogEventActionChangeHistoryTTL#6e941a38 prev_value:int new_value:int = ChannelAdminLogEventAction; +channelAdminLogEventActionChangeTheme#fe69018d prev_value:string new_value:string = ChannelAdminLogEventAction; -channelAdminLogEvent#3b5a3e40 id:long date:int user_id:int action:ChannelAdminLogEventAction = ChannelAdminLogEvent; +channelAdminLogEvent#1fad68cd id:long date:int user_id:long action:ChannelAdminLogEventAction = ChannelAdminLogEvent; channels.adminLogResults#ed8af74d events:Vector chats:Vector users:Vector = channels.AdminLogResults; @@ -914,11 +918,11 @@ channelAdminLogEventsFilter#ea107ae4 flags:# join:flags.0?true leave:flags.1?tru popularContact#5ce14175 client_id:long importers:int = PopularContact; messages.favedStickersNotModified#9e8fa6d3 = messages.FavedStickers; -messages.favedStickers#f37f2f16 hash:int packs:Vector stickers:Vector = messages.FavedStickers; +messages.favedStickers#2cb51097 hash:long packs:Vector stickers:Vector = messages.FavedStickers; recentMeUrlUnknown#46e1d13d url:string = RecentMeUrl; -recentMeUrlUser#8dbc3336 url:string user_id:int = RecentMeUrl; -recentMeUrlChat#a01b22f9 url:string chat_id:int = RecentMeUrl; +recentMeUrlUser#b92c09e2 url:string user_id:long = RecentMeUrl; +recentMeUrlChat#b2da71d2 url:string chat_id:long = RecentMeUrl; recentMeUrlChatInvite#eb49081d url:string chat_invite:ChatInvite = RecentMeUrl; recentMeUrlStickerSet#bc0a57dc url:string set:StickerSetCovered = RecentMeUrl; @@ -926,7 +930,7 @@ help.recentMeUrls#e0310d7 urls:Vector chats:Vector users:Vect inputSingleMedia#1cc6e91f flags:# media:InputMedia random_id:long message:string entities:flags.0?Vector = InputSingleMedia; -webAuthorization#cac943f2 hash:long bot_id:int domain:string browser:string platform:string date_created:int date_active:int ip:string region:string = WebAuthorization; +webAuthorization#a6f8f452 hash:long bot_id:long domain:string browser:string platform:string date_created:int date_active:int ip:string region:string = WebAuthorization; account.webAuthorizations#ed56c9fc authorizations:Vector users:Vector = account.WebAuthorizations; @@ -942,7 +946,7 @@ dialogPeer#e56dbf05 peer:Peer = DialogPeer; dialogPeerFolder#514519e2 folder_id:int = DialogPeer; messages.foundStickerSetsNotModified#d54b65d = messages.FoundStickerSets; -messages.foundStickerSets#5108d648 hash:int sets:Vector = messages.FoundStickerSets; +messages.foundStickerSets#8af09dd2 hash:long sets:Vector = messages.FoundStickerSets; fileHash#6242c773 offset:int limit:int hash:bytes = FileHash; @@ -1061,7 +1065,7 @@ poll#86e18161 id:long flags:# closed:flags.0?true public_voters:flags.1?true mul pollAnswerVoters#3b6ddad2 flags:# chosen:flags.0?true correct:flags.1?true option:bytes voters:int = PollAnswerVoters; -pollResults#badcc1a3 flags:# min:flags.0?true results:flags.1?Vector total_voters:flags.2?int recent_voters:flags.3?Vector solution:flags.4?string solution_entities:flags.4?Vector = PollResults; +pollResults#dcb82ea3 flags:# min:flags.0?true results:flags.1?Vector total_voters:flags.2?int recent_voters:flags.3?Vector solution:flags.4?string solution_entities:flags.4?Vector = PollResults; chatOnlines#f041e250 onlines:int = ChatOnlines; @@ -1076,7 +1080,7 @@ inputWallPaperSlug#72091c80 slug:string = InputWallPaper; inputWallPaperNoFile#967a462e id:long = InputWallPaper; account.wallPapersNotModified#1c199183 = account.WallPapers; -account.wallPapers#702b65a9 hash:int wallpapers:Vector = account.WallPapers; +account.wallPapers#cdc3858c hash:long wallpapers:Vector = account.WallPapers; codeSettings#debebe83 flags:# allow_flashcall:flags.0?true current_number:flags.1?true allow_app_hash:flags.4?true = CodeSettings; @@ -1121,7 +1125,7 @@ inputThemeSlug#f5890df1 slug:string = InputTheme; theme#e802b8dc flags:# creator:flags.0?true default:flags.1?true for_chat:flags.5?true id:long access_hash:long slug:string title:string document:flags.2?Document settings:flags.3?ThemeSettings installs_count:flags.4?int = Theme; account.themesNotModified#f41eb622 = account.Themes; -account.themes#7f676421 hash:int themes:Vector = account.Themes; +account.themes#9a3d8c6d hash:long themes:Vector = account.Themes; auth.loginToken#629f1980 expires:int token:bytes = auth.LoginToken; auth.loginTokenMigrateTo#68e9916 dc_id:int token:bytes = auth.LoginToken; @@ -1137,15 +1141,15 @@ baseThemeNight#b7b31ea8 = BaseTheme; baseThemeTinted#6d5f77ee = BaseTheme; baseThemeArctic#5b11125a = BaseTheme; -inputThemeSettings#ff38f912 flags:# message_colors_animated:flags.2?true base_theme:BaseTheme accent_color:int message_colors:flags.0?Vector wallpaper:flags.1?InputWallPaper wallpaper_settings:flags.1?WallPaperSettings = InputThemeSettings; +inputThemeSettings#8fde504f flags:# message_colors_animated:flags.2?true base_theme:BaseTheme accent_color:int outbox_accent_color:flags.3?int message_colors:flags.0?Vector wallpaper:flags.1?InputWallPaper wallpaper_settings:flags.1?WallPaperSettings = InputThemeSettings; -themeSettings#8db4e76c flags:# message_colors_animated:flags.2?true base_theme:BaseTheme accent_color:int message_colors:flags.0?Vector wallpaper:flags.1?WallPaper = ThemeSettings; +themeSettings#fa58b6d4 flags:# message_colors_animated:flags.2?true base_theme:BaseTheme accent_color:int outbox_accent_color:flags.3?int message_colors:flags.0?Vector wallpaper:flags.1?WallPaper = ThemeSettings; webPageAttributeTheme#54b56617 flags:# documents:flags.0?Vector settings:flags.1?ThemeSettings = WebPageAttribute; -messageUserVote#a28e5559 user_id:int option:bytes date:int = MessageUserVote; -messageUserVoteInputOption#36377430 user_id:int date:int = MessageUserVote; -messageUserVoteMultiple#e8fe0de user_id:int options:Vector date:int = MessageUserVote; +messageUserVote#34d247b4 user_id:long option:bytes date:int = MessageUserVote; +messageUserVoteInputOption#3ca5b0ec user_id:long date:int = MessageUserVote; +messageUserVoteMultiple#8a65e557 user_id:long options:Vector date:int = MessageUserVote; messages.votesList#823f649 flags:# count:int votes:Vector users:Vector next_offset:flags.0?string = messages.VotesList; @@ -1176,11 +1180,11 @@ help.promoData#8c39793f flags:# proxy:flags.0?true expires:int peer:Peer chats:V videoSize#de33b094 flags:# type:string w:int h:int size:int video_start_ts:flags.0?double = VideoSize; -statsGroupTopPoster#18f3d0f7 user_id:int messages:int avg_chars:int = StatsGroupTopPoster; +statsGroupTopPoster#9d04af9b user_id:long messages:int avg_chars:int = StatsGroupTopPoster; -statsGroupTopAdmin#6014f412 user_id:int deleted:int kicked:int banned:int = StatsGroupTopAdmin; +statsGroupTopAdmin#d7584c87 user_id:long deleted:int kicked:int banned:int = StatsGroupTopAdmin; -statsGroupTopInviter#31962a4c user_id:int invitations:int = StatsGroupTopInviter; +statsGroupTopInviter#535f779d user_id:long invitations:int = StatsGroupTopInviter; stats.megagroupStats#ef7ff916 period:StatsDateRangeDays members:StatsAbsValueAndPrev messages:StatsAbsValueAndPrev viewers:StatsAbsValueAndPrev posters:StatsAbsValueAndPrev growth_graph:StatsGraph members_graph:StatsGraph new_members_by_source_graph:StatsGraph languages_graph:StatsGraph messages_graph:StatsGraph actions_graph:StatsGraph top_hours_graph:StatsGraph weekdays_graph:StatsGraph top_posters:Vector top_admins:Vector top_inviters:Vector users:Vector = stats.MegagroupStats; @@ -1201,7 +1205,7 @@ messages.discussionMessage#a6341782 flags:# messages:Vector max_id:flag messageReplyHeader#a6d57763 flags:# reply_to_msg_id:int reply_to_peer_id:flags.0?Peer reply_to_top_id:flags.1?int = MessageReplyHeader; -messageReplies#4128faac flags:# comments:flags.0?true replies:int replies_pts:int recent_repliers:flags.1?Vector channel_id:flags.0?int max_id:flags.2?int read_max_id:flags.3?int = MessageReplies; +messageReplies#83d60fc2 flags:# comments:flags.0?true replies:int replies_pts:int recent_repliers:flags.1?Vector channel_id:flags.0?long max_id:flags.2?int read_max_id:flags.3?int = MessageReplies; peerBlocked#e8fd8014 peer_id:Peer date:int = PeerBlocked; @@ -1230,7 +1234,7 @@ messages.historyImportParsed#5e0fb7b9 flags:# pm:flags.0?true group:flags.1?true messages.affectedFoundMessages#ef8d3e6c pts:int pts_count:int offset:int messages:Vector = messages.AffectedFoundMessages; -chatInviteImporter#1e3e6680 user_id:int date:int = ChatInviteImporter; +chatInviteImporter#b5cd5f4 user_id:long date:int = ChatInviteImporter; messages.exportedChatInvites#bdc62dcc count:int invites:Vector users:Vector = messages.ExportedChatInvites; @@ -1239,7 +1243,7 @@ messages.exportedChatInviteReplaced#222600ef invite:ExportedChatInvite new_invit messages.chatInviteImporters#81b6b00a count:int importers:Vector users:Vector = messages.ChatInviteImporters; -chatAdminWithInvites#dfd2330f admin_id:int invites_count:int revoked_invites_count:int = ChatAdminWithInvites; +chatAdminWithInvites#f2ecef23 admin_id:long invites_count:int revoked_invites_count:int = ChatAdminWithInvites; messages.chatAdminsWithInvites#b69b72d7 admins:Vector users:Vector = messages.ChatAdminsWithInvites; @@ -1272,7 +1276,7 @@ chatTheme#ed0b5c33 emoticon:string theme:Theme dark_theme:Theme = ChatTheme; account.chatThemesNotModified#e011e1c4 = account.ChatThemes; account.chatThemes#fe4cbebd hash:int themes:Vector = account.ChatThemes; -sponsoredMessage#f671f0d1 flags:# random_id:bytes peer_id:Peer from_id:Peer message:string media:flags.0?MessageMedia entities:flags.1?Vector = SponsoredMessage; +sponsoredMessage#2a3c381f flags:# random_id:bytes from_id:Peer start_param:flags.0?string message:string entities:flags.1?Vector = SponsoredMessage; messages.sponsoredMessages#65a4c7d5 messages:Vector chats:Vector users:Vector = messages.SponsoredMessages; @@ -1292,7 +1296,7 @@ auth.signIn#bcd51581 phone_number:string phone_code_hash:string phone_code:strin auth.logOut#5717da40 = Bool; auth.resetAuthorizations#9fab0d1a = Bool; auth.exportAuthorization#e5bfffcd dc_id:int = auth.ExportedAuthorization; -auth.importAuthorization#e3ef9613 id:int bytes:bytes = auth.Authorization; +auth.importAuthorization#a57a7dad id:long bytes:bytes = auth.Authorization; auth.bindTempAuthKey#cdd42a05 perm_auth_key_id:long nonce:long expires_at:int encrypted_message:bytes = Bool; auth.importBotAuthorization#67a3ff2c flags:int api_id:int api_hash:string bot_auth_token:string = auth.Authorization; auth.checkPassword#d18b4d16 password:InputCheckPasswordSRP = auth.Authorization; @@ -1301,19 +1305,19 @@ auth.recoverPassword#37096c70 flags:# code:string new_settings:flags.0?account.P auth.resendCode#3ef1a9bf phone_number:string phone_code_hash:string = auth.SentCode; auth.cancelCode#1f040578 phone_number:string phone_code_hash:string = Bool; auth.dropTempAuthKeys#8e48a188 except_auth_keys:Vector = Bool; -auth.exportLoginToken#b1b41517 api_id:int api_hash:string except_ids:Vector = auth.LoginToken; +auth.exportLoginToken#b7e085fe api_id:int api_hash:string except_ids:Vector = auth.LoginToken; auth.importLoginToken#95ac5ce4 token:bytes = auth.LoginToken; auth.acceptLoginToken#e894ad4d token:bytes = Authorization; auth.checkRecoveryPassword#d36bf79 code:string = Bool; -account.registerDevice#68976c6f flags:# no_muted:flags.0?true token_type:int token:string app_sandbox:Bool secret:bytes other_uids:Vector = Bool; -account.unregisterDevice#3076c4bf token_type:int token:string other_uids:Vector = Bool; +account.registerDevice#ec86017a flags:# no_muted:flags.0?true token_type:int token:string app_sandbox:Bool secret:bytes other_uids:Vector = Bool; +account.unregisterDevice#6a0d3206 token_type:int token:string other_uids:Vector = Bool; account.updateNotifySettings#84be5b93 peer:InputNotifyPeer settings:InputPeerNotifySettings = Bool; account.getNotifySettings#12b3ad31 peer:InputNotifyPeer = PeerNotifySettings; account.resetNotifySettings#db7e1747 = Bool; account.updateProfile#78515775 flags:# first_name:flags.0?string last_name:flags.1?string about:flags.2?string = User; account.updateStatus#6628562c offline:Bool = Bool; -account.getWallPapers#aabb1763 hash:int = account.WallPapers; +account.getWallPapers#7967d36 hash:long = account.WallPapers; account.reportPeer#c5ba3d86 peer:InputPeer reason:ReportReason message:string = Bool; account.checkUsername#2714d86c username:string = Bool; account.updateUsername#3e0bdd7c username:string = User; @@ -1340,8 +1344,8 @@ account.getAllSecureValues#b288bc7d = Vector; account.getSecureValue#73665bc2 types:Vector = Vector; account.saveSecureValue#899fe31d value:InputSecureValue secure_secret_id:long = SecureValue; account.deleteSecureValue#b880bc4b types:Vector = Bool; -account.getAuthorizationForm#b86ba8e1 bot_id:int scope:string public_key:string = account.AuthorizationForm; -account.acceptAuthorization#e7027c94 bot_id:int scope:string public_key:string value_hashes:Vector credentials:SecureCredentialsEncrypted = Bool; +account.getAuthorizationForm#a929597a bot_id:long scope:string public_key:string = account.AuthorizationForm; +account.acceptAuthorization#f3ed4c73 bot_id:long scope:string public_key:string value_hashes:Vector credentials:SecureCredentialsEncrypted = Bool; account.sendVerifyPhoneCode#a5a356f9 phone_number:string settings:CodeSettings = auth.SentCode; account.verifyPhone#4dd3a7f6 phone_number:string phone_code_hash:string phone_code:string = Bool; account.sendVerifyEmailCode#7011509f email:string = account.SentEmailCode; @@ -1367,7 +1371,7 @@ account.updateTheme#5cb367d5 flags:# format:string theme:InputTheme slug:flags.0 account.saveTheme#f257106c theme:InputTheme unsave:Bool = Bool; account.installTheme#7ae43737 flags:# dark:flags.0?true format:flags.1?string theme:flags.1?InputTheme = Bool; account.getTheme#8d9d742b format:string theme:InputTheme document_id:long = Theme; -account.getThemes#285946f8 format:string hash:int = account.Themes; +account.getThemes#7206e458 format:string hash:long = account.Themes; account.setContentSettings#b574b16b flags:# sensitive_enabled:flags.0?true = Bool; account.getContentSettings#8b9b4dae = account.ContentSettings; account.getMultiWallPapers#65ad71dc wallpapers:Vector = Vector; @@ -1382,9 +1386,9 @@ users.getUsers#d91a548 id:Vector = Vector; users.getFullUser#ca30a5b1 id:InputUser = UserFull; users.setSecureValueErrors#90c894b5 id:InputUser errors:Vector = Bool; -contacts.getContactIDs#2caa4a42 hash:int = Vector; +contacts.getContactIDs#7adc669d hash:long = Vector; contacts.getStatuses#c4a353ee = Vector; -contacts.getContacts#c023849f hash:int = contacts.Contacts; +contacts.getContacts#5dd69e12 hash:long = contacts.Contacts; contacts.importContacts#2c800be5 contacts:Vector = contacts.ImportedContacts; contacts.deleteContacts#96a0e00 id:Vector = Updates; contacts.deleteByPhones#1013fd9e phones:Vector = Bool; @@ -1393,7 +1397,7 @@ contacts.unblock#bea65d50 id:InputPeer = Bool; contacts.getBlocked#f57c350f offset:int limit:int = contacts.Blocked; contacts.search#11f812d8 q:string limit:int = contacts.Found; contacts.resolveUsername#f93ccba3 username:string = contacts.ResolvedPeer; -contacts.getTopPeers#d4982db5 flags:# correspondents:flags.0?true bots_pm:flags.1?true bots_inline:flags.2?true phone_calls:flags.3?true forward_users:flags.4?true forward_chats:flags.5?true groups:flags.10?true channels:flags.15?true offset:int limit:int hash:int = contacts.TopPeers; +contacts.getTopPeers#973478b6 flags:# correspondents:flags.0?true bots_pm:flags.1?true bots_inline:flags.2?true phone_calls:flags.3?true forward_users:flags.4?true forward_chats:flags.5?true groups:flags.10?true channels:flags.15?true offset:int limit:int hash:long = contacts.TopPeers; contacts.resetTopPeerRating#1ae373ac category:TopPeerCategory peer:InputPeer = Bool; contacts.resetSaved#879537f1 = Bool; contacts.getSaved#82f1e39f = Vector; @@ -1404,9 +1408,9 @@ contacts.getLocated#d348bc44 flags:# background:flags.1?true geo_point:InputGeoP contacts.blockFromReplies#29a8962c flags:# delete_message:flags.0?true delete_history:flags.1?true report_spam:flags.2?true msg_id:int = Updates; messages.getMessages#63c66506 id:Vector = messages.Messages; -messages.getDialogs#a0ee3b73 flags:# exclude_pinned:flags.0?true folder_id:flags.1?int offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:int = messages.Dialogs; -messages.getHistory#dcbb8260 peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:int = messages.Messages; -messages.search#c352eec flags:# peer:InputPeer q:string from_id:flags.0?InputPeer top_msg_id:flags.1?int filter:MessagesFilter min_date:int max_date:int offset_id:int add_offset:int limit:int max_id:int min_id:int hash:int = messages.Messages; +messages.getDialogs#a0f4cb4f flags:# exclude_pinned:flags.0?true folder_id:flags.1?int offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:long = messages.Dialogs; +messages.getHistory#4423e6c5 peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages; +messages.search#a0fda762 flags:# peer:InputPeer q:string from_id:flags.0?InputPeer top_msg_id:flags.1?int filter:MessagesFilter min_date:int max_date:int offset_id:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages; messages.readHistory#e306d3a peer:InputPeer max_id:int = messages.AffectedMessages; messages.deleteHistory#1c015b09 flags:# just_clear:flags.0?true revoke:flags.1?true peer:InputPeer max_id:int = messages.AffectedHistory; messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector = messages.AffectedMessages; @@ -1418,12 +1422,12 @@ messages.forwardMessages#d9fee60e flags:# silent:flags.5?true background:flags.6 messages.reportSpam#cf1592db peer:InputPeer = Bool; messages.getPeerSettings#3672e09c peer:InputPeer = PeerSettings; messages.report#8953ab4e peer:InputPeer id:Vector reason:ReportReason message:string = Bool; -messages.getChats#3c6aa187 id:Vector = messages.Chats; -messages.getFullChat#3b831c66 chat_id:int = messages.ChatFull; -messages.editChatTitle#dc452855 chat_id:int title:string = Updates; -messages.editChatPhoto#ca4c79d8 chat_id:int photo:InputChatPhoto = Updates; -messages.addChatUser#f9a0aa09 chat_id:int user_id:InputUser fwd_limit:int = Updates; -messages.deleteChatUser#c534459a flags:# revoke_history:flags.0?true chat_id:int user_id:InputUser = Updates; +messages.getChats#49e9528f id:Vector = messages.Chats; +messages.getFullChat#aeb00b34 chat_id:long = messages.ChatFull; +messages.editChatTitle#73783ffd chat_id:long title:string = Updates; +messages.editChatPhoto#35ddd674 chat_id:long photo:InputChatPhoto = Updates; +messages.addChatUser#f24753e3 chat_id:long user_id:InputUser fwd_limit:int = Updates; +messages.deleteChatUser#a2185cab flags:# revoke_history:flags.0?true chat_id:long user_id:InputUser = Updates; messages.createChat#9cb126e users:Vector title:string = Updates; messages.getDhConfig#26cf8950 version:int random_length:int = messages.DhConfig; messages.requestEncryption#f64daf43 user_id:InputUser random_id:int g_a:bytes = EncryptedChat; @@ -1437,8 +1441,8 @@ messages.sendEncryptedService#32d439a4 peer:InputEncryptedChat random_id:long da messages.receivedQueue#55a5bb66 max_qts:int = Vector; messages.reportEncryptedSpam#4b0c8c0f peer:InputEncryptedChat = Bool; messages.readMessageContents#36a73f77 id:Vector = messages.AffectedMessages; -messages.getStickers#43d4f2c emoticon:string hash:int = messages.Stickers; -messages.getAllStickers#1c9618b1 hash:int = messages.AllStickers; +messages.getStickers#d5a5d3a1 emoticon:string hash:long = messages.Stickers; +messages.getAllStickers#b8a0a1a8 hash:long = messages.AllStickers; messages.getWebPagePreview#8b68b0cc flags:# message:string entities:flags.3?Vector = MessageMedia; messages.exportChatInvite#14b9bcd7 flags:# legacy_revoke_permanent:flags.2?true peer:InputPeer expire_date:flags.0?int usage_limit:flags.1?int = ExportedChatInvite; messages.checkChatInvite#3eadb1bb hash:string = ChatInvite; @@ -1448,12 +1452,12 @@ messages.installStickerSet#c78fe460 stickerset:InputStickerSet archived:Bool = m messages.uninstallStickerSet#f96e55de stickerset:InputStickerSet = Bool; messages.startBot#e6df7378 bot:InputUser peer:InputPeer random_id:long start_param:string = Updates; messages.getMessagesViews#5784d3e1 peer:InputPeer id:Vector increment:Bool = messages.MessageViews; -messages.editChatAdmin#a9e69f2e chat_id:int user_id:InputUser is_admin:Bool = Bool; -messages.migrateChat#15a3b8e3 chat_id:int = Updates; +messages.editChatAdmin#a85bd1c2 chat_id:long user_id:InputUser is_admin:Bool = Bool; +messages.migrateChat#a2875319 chat_id:long = Updates; messages.searchGlobal#4bc6589a flags:# folder_id:flags.0?int q:string filter:MessagesFilter min_date:int max_date:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; messages.reorderStickerSets#78337739 flags:# masks:flags.0?true order:Vector = Bool; messages.getDocumentByHash#338e2464 sha256:bytes size:int mime_type:string = Document; -messages.getSavedGifs#83bf3d52 hash:int = messages.SavedGifs; +messages.getSavedGifs#5cf09635 hash:long = messages.SavedGifs; messages.saveGif#327a30cb id:InputDocument unsave:Bool = Bool; messages.getInlineBotResults#514e999d flags:# bot:InputUser peer:InputPeer geo_point:flags.0?InputGeoPoint query:string offset:string = messages.BotResults; messages.setInlineBotResults#eb5ea206 flags:# gallery:flags.0?true private:flags.1?true query_id:long results:Vector cache_time:int next_offset:flags.2?string switch_pm:flags.3?InlineBotSwitchPM = Bool; @@ -1466,20 +1470,20 @@ messages.setBotCallbackAnswer#d58f130a flags:# alert:flags.1?true query_id:long messages.getPeerDialogs#e470bcfd peers:Vector = messages.PeerDialogs; messages.saveDraft#bc39e14b flags:# no_webpage:flags.1?true reply_to_msg_id:flags.0?int peer:InputPeer message:string entities:flags.3?Vector = Bool; messages.getAllDrafts#6a3f8d65 = Updates; -messages.getFeaturedStickers#2dacca4f hash:int = messages.FeaturedStickers; +messages.getFeaturedStickers#64780b14 hash:long = messages.FeaturedStickers; messages.readFeaturedStickers#5b118126 id:Vector = Bool; -messages.getRecentStickers#5ea192c9 flags:# attached:flags.0?true hash:int = messages.RecentStickers; +messages.getRecentStickers#9da9403b flags:# attached:flags.0?true hash:long = messages.RecentStickers; messages.saveRecentSticker#392718f8 flags:# attached:flags.0?true id:InputDocument unsave:Bool = Bool; messages.clearRecentStickers#8999602d flags:# attached:flags.0?true = Bool; messages.getArchivedStickers#57f17692 flags:# masks:flags.0?true offset_id:long limit:int = messages.ArchivedStickers; -messages.getMaskStickers#65b8c79f hash:int = messages.AllStickers; +messages.getMaskStickers#640f82b8 hash:long = messages.AllStickers; messages.getAttachedStickers#cc5b67cc media:InputStickeredMedia = Vector; messages.setGameScore#8ef8ecc0 flags:# edit_message:flags.0?true force:flags.1?true peer:InputPeer id:int user_id:InputUser score:int = Updates; messages.setInlineGameScore#15ad9f64 flags:# edit_message:flags.0?true force:flags.1?true id:InputBotInlineMessageID user_id:InputUser score:int = Bool; messages.getGameHighScores#e822649d peer:InputPeer id:int user_id:InputUser = messages.HighScores; messages.getInlineGameHighScores#f635e1b id:InputBotInlineMessageID user_id:InputUser = messages.HighScores; -messages.getCommonChats#d0a48c4 user_id:InputUser max_id:int limit:int = messages.Chats; -messages.getAllChats#eba80ff0 except_ids:Vector = messages.Chats; +messages.getCommonChats#e40ca104 user_id:InputUser max_id:long limit:int = messages.Chats; +messages.getAllChats#875f74be except_ids:Vector = messages.Chats; messages.getWebPage#32ca8f91 url:string hash:int = WebPage; messages.toggleDialogPin#a731e257 flags:# pinned:flags.0?true peer:InputDialogPeer = Bool; messages.reorderPinnedDialogs#3b1adf37 flags:# force:flags.0?true folder_id:int order:Vector = Bool; @@ -1488,14 +1492,14 @@ messages.setBotShippingResults#e5f672fa flags:# query_id:long error:flags.0?stri messages.setBotPrecheckoutResults#9c2dd95 flags:# success:flags.1?true query_id:long error:flags.0?string = Bool; messages.uploadMedia#519bc2b1 peer:InputPeer media:InputMedia = MessageMedia; messages.sendScreenshotNotification#c97df020 peer:InputPeer reply_to_msg_id:int random_id:long = Updates; -messages.getFavedStickers#21ce0b0e hash:int = messages.FavedStickers; +messages.getFavedStickers#4f1aaa9 hash:long = messages.FavedStickers; messages.faveSticker#b9ffc55b id:InputDocument unfave:Bool = Bool; messages.getUnreadMentions#46578472 peer:InputPeer offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; messages.readMentions#f0189d3 peer:InputPeer = messages.AffectedHistory; -messages.getRecentLocations#bbc45b09 peer:InputPeer limit:int hash:int = messages.Messages; +messages.getRecentLocations#702a40e0 peer:InputPeer limit:int hash:long = messages.Messages; messages.sendMultiMedia#cc0110cb flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int multi_media:Vector schedule_date:flags.10?int = Updates; messages.uploadEncryptedFile#5057c497 peer:InputEncryptedChat file:InputEncryptedFile = EncryptedFile; -messages.searchStickerSets#c2b7d08b flags:# exclude_featured:flags.0?true q:string hash:int = messages.FoundStickerSets; +messages.searchStickerSets#35705b8a flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets; messages.getSplitRanges#1cff7e08 = Vector; messages.markDialogUnread#c286d98f flags:# unread:flags.0?true peer:InputDialogPeer = Bool; messages.getDialogUnreadMarks#22e24e22 = Vector; @@ -1515,7 +1519,7 @@ messages.getSearchCounters#732eef00 peer:InputPeer filters:Vector = messages.Messages; messages.sendScheduledMessages#bd38850a peer:InputPeer id:Vector = Updates; messages.deleteScheduledMessages#59ae2b16 peer:InputPeer id:Vector = Updates; @@ -1525,12 +1529,12 @@ messages.getDialogFilters#f19ed96d = Vector; messages.getSuggestedDialogFilters#a29cd42c = Vector; messages.updateDialogFilter#1ad4a04a flags:# id:int filter:flags.0?DialogFilter = Bool; messages.updateDialogFiltersOrder#c563c1e4 order:Vector = Bool; -messages.getOldFeaturedStickers#5fe7025b offset:int limit:int hash:int = messages.FeaturedStickers; -messages.getReplies#24b581ba peer:InputPeer msg_id:int offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:int = messages.Messages; +messages.getOldFeaturedStickers#7ed094a1 offset:int limit:int hash:long = messages.FeaturedStickers; +messages.getReplies#22ddd30c peer:InputPeer msg_id:int offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages; messages.getDiscussionMessage#446972fd peer:InputPeer msg_id:int = messages.DiscussionMessage; messages.readDiscussion#f731a9f4 peer:InputPeer msg_id:int read_max_id:int = Bool; messages.unpinAllMessages#f025bc8b peer:InputPeer = messages.AffectedHistory; -messages.deleteChat#83247d11 chat_id:int = Bool; +messages.deleteChat#5bd0ee50 chat_id:long = Bool; messages.deletePhoneCallHistory#f9cbe409 flags:# revoke:flags.0?true = messages.AffectedFoundMessages; messages.checkHistoryImport#43fe19f3 import_head:string = messages.HistoryImportParsed; messages.initHistoryImport#34090c3b peer:InputPeer file:InputFile media_count:int = messages.HistoryImport; @@ -1546,6 +1550,7 @@ messages.getChatInviteImporters#26fb7289 peer:InputPeer link:string offset_date: messages.setHistoryTTL#b80e5fe4 peer:InputPeer period:int = Updates; messages.checkHistoryImportPeer#5dc60f03 peer:InputPeer = messages.CheckedHistoryImportPeer; messages.setChatTheme#e63be13f peer:InputPeer emoticon:string = Updates; +messages.getMessageReadParticipants#2c6f97b7 peer:InputPeer msg_id:int = Vector; updates.getState#edd4882a = updates.State; updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference; @@ -1593,7 +1598,7 @@ channels.deleteMessages#84c1fd4e channel:InputChannel id:Vector = messages. channels.deleteUserHistory#d10dd71b channel:InputChannel user_id:InputUser = messages.AffectedHistory; channels.reportSpam#fe087810 channel:InputChannel user_id:InputUser id:Vector = Bool; channels.getMessages#ad8c9a23 channel:InputChannel id:Vector = messages.Messages; -channels.getParticipants#123e05e9 channel:InputChannel filter:ChannelParticipantsFilter offset:int limit:int hash:int = channels.ChannelParticipants; +channels.getParticipants#77ced9d0 channel:InputChannel filter:ChannelParticipantsFilter offset:int limit:int hash:long = channels.ChannelParticipants; channels.getParticipant#a0ab6cc6 channel:InputChannel participant:InputPeer = channels.ChannelParticipant; channels.getChannels#a7f6bbb id:Vector = messages.Chats; channels.getFullChannel#8736a09 channel:InputChannel = messages.ChatFull; @@ -1693,4 +1698,4 @@ stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel stats.getMessagePublicForwards#5630281b channel:InputChannel msg_id:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats; -// LAYER 132 +// LAYER 133 diff --git a/telethon_generator/data/errors.csv b/telethon_generator/data/errors.csv index 6e50f683..1f2120e2 100644 --- a/telethon_generator/data/errors.csv +++ b/telethon_generator/data/errors.csv @@ -77,6 +77,7 @@ CHAT_SEND_INLINE_FORBIDDEN,400,You cannot send inline results in this chat CHAT_SEND_MEDIA_FORBIDDEN,403,You can't send media in this chat CHAT_SEND_STICKERS_FORBIDDEN,403,You can't send stickers in this chat CHAT_TITLE_EMPTY,400,No chat title provided +CHAT_TOO_BIG,400, CHAT_WRITE_FORBIDDEN,403,You can't write in this chat CHP_CALL_FAIL,500,The statistics cannot be retrieved at this time CODE_EMPTY,400,The provided code is empty diff --git a/telethon_generator/data/methods.csv b/telethon_generator/data/methods.csv index 9066202d..266e352a 100644 --- a/telethon_generator/data/methods.csv +++ b/telethon_generator/data/methods.csv @@ -226,6 +226,7 @@ messages.getInlineGameHighScores,bot,MESSAGE_ID_INVALID USER_BOT_REQUIRED messages.getMaskStickers,user, messages.getMessageEditData,user,MESSAGE_AUTHOR_REQUIRED PEER_ID_INVALID messages.getMessages,both, +messages.getMessagesReadParticipants,user,CHAT_TOO_BIG messages.getMessagesViews,user,CHANNEL_PRIVATE CHAT_ID_INVALID PEER_ID_INVALID messages.getOnlines,user, messages.getPeerDialogs,user,CHANNEL_PRIVATE PEER_ID_INVALID From 3f5f5dbe48d1b80baab78692e95b10bfd78cb6dc Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 11 Sep 2021 11:02:10 +0200 Subject: [PATCH 030/256] Update documentation and list of known errors Closes #3151. --- readthedocs/examples/chats-and-channels.rst | 4 ++++ readthedocs/quick-references/client-reference.rst | 1 + telethon/client/dialogs.py | 10 ++++++++++ telethon_generator/data/errors.csv | 3 +++ telethon_generator/data/methods.csv | 3 ++- 5 files changed, 20 insertions(+), 1 deletion(-) diff --git a/readthedocs/examples/chats-and-channels.rst b/readthedocs/examples/chats-and-channels.rst index 53b508be..2c3823c3 100644 --- a/readthedocs/examples/chats-and-channels.rst +++ b/readthedocs/examples/chats-and-channels.rst @@ -84,6 +84,10 @@ use is very straightforward, or :tl:`InviteToChannelRequest` for channels: [users_to_add] )) +Note that this method will only really work for friends or bot accounts. +Trying to mass-add users with this approach will not work, and can put both +your account and group to risk, possibly being flagged as spam and limited. + Checking a link without joining =============================== diff --git a/readthedocs/quick-references/client-reference.rst b/readthedocs/quick-references/client-reference.rst index 5b998344..6dd8245c 100644 --- a/readthedocs/quick-references/client-reference.rst +++ b/readthedocs/quick-references/client-reference.rst @@ -169,6 +169,7 @@ Updates remove_event_handler list_event_handlers catch_up + set_receive_updates Bots ---- diff --git a/telethon/client/dialogs.py b/telethon/client/dialogs.py index 3665c94e..8c0860fc 100644 --- a/telethon/client/dialogs.py +++ b/telethon/client/dialogs.py @@ -482,6 +482,16 @@ class DialogMethods: Creates a `Conversation ` with the given entity. + .. 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. diff --git a/telethon_generator/data/errors.csv b/telethon_generator/data/errors.csv index 1f2120e2..ff9d8168 100644 --- a/telethon_generator/data/errors.csv +++ b/telethon_generator/data/errors.csv @@ -151,6 +151,7 @@ GIF_CONTENT_TYPE_INVALID,400, GIF_ID_INVALID,400,The provided GIF ID is invalid GRAPH_INVALID_RELOAD,400, GRAPH_OUTDATED_RELOAD,400,"Data can't be used for the channel statistics, graphs outdated" +GROUPCALL_ADD_PARTICIPANTS_FAILED,500, GROUPCALL_ALREADY_DISCARDED,400, GROUPCALL_FORBIDDEN,403, GROUPCALL_JOIN_MISSING,400, @@ -359,9 +360,11 @@ TMP_PASSWORD_DISABLED,400,The temporary password is disabled TMP_PASSWORD_INVALID,400,Password auth needs to be regenerated TOKEN_INVALID,400,The provided token is invalid TTL_DAYS_INVALID,400,The provided TTL is invalid +TTL_MEDIA_INVALID,400,The provided media cannot be used with a TTL TTL_PERIOD_INVALID,400,The provided TTL Period is invalid TYPES_EMPTY,400,The types field is empty TYPE_CONSTRUCTOR_INVALID,400,The type constructor is invalid +UNKNOWN_ERROR,400, UNKNOWN_METHOD,500,The method you tried to call cannot be called on non-CDN DCs UNTIL_DATE_INVALID,400,That date cannot be specified in this request (try using None) URL_INVALID,400,The URL used was invalid (e.g. when answering a callback with a URL that's not t.me/yourbot or your game's URL) diff --git a/telethon_generator/data/methods.csv b/telethon_generator/data/methods.csv index 266e352a..4427492b 100644 --- a/telethon_generator/data/methods.csv +++ b/telethon_generator/data/methods.csv @@ -124,6 +124,7 @@ channels.togglePreHistoryHidden,user,CHAT_LINK_EXISTS channels.toggleSignatures,user,CHANNEL_INVALID channels.toggleSlowMode,user,SECONDS_INVALID channels.updateUsername,user,CHANNELS_ADMIN_PUBLIC_TOO_MUCH CHANNEL_INVALID CHAT_ADMIN_REQUIRED USERNAME_INVALID USERNAME_OCCUPIED +channels.viewSponsoredMessage,user,UNKNOWN_ERROR contacts.acceptContact,user, contacts.addContact,user,CONTACT_NAME_EMPTY contacts.block,user,CONTACT_ID_INVALID @@ -319,7 +320,7 @@ phone.discardGroupCallRequest,user,GROUPCALL_ALREADY_DISCARDED phone.editGroupCallParticipant,user,USER_VOLUME_INVALID phone.getCallConfig,user, phone.inviteToGroupCall,user,GROUPCALL_FORBIDDEN -phone.joinGroupCall,user,GROUPCALL_SSRC_DUPLICATE_MUCH +phone.joinGroupCall,user,GROUPCALL_ADD_PARTICIPANTS_FAILED GROUPCALL_SSRC_DUPLICATE_MUCH phone.joinGroupCallPresentation,user, PARTICIPANT_JOIN_MISSING phone.receivedCall,user,CALL_ALREADY_DECLINED CALL_PEER_INVALID phone.requestCall,user,CALL_PROTOCOL_FLAGS_INVALID PARTICIPANT_CALL_FAILED PARTICIPANT_VERSION_OUTDATED USER_ID_INVALID USER_IS_BLOCKED USER_PRIVACY_RESTRICTED From 2e1be01ad4f6462de2e9e1f96a33537e51f44980 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 11 Sep 2021 11:02:19 +0200 Subject: [PATCH 031/256] Add ttl parameter to send_file --- telethon/client/uploads.py | 36 ++++++++++++++++++++++++++---------- telethon/utils.py | 24 ++++++++++++++---------- 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/telethon/client/uploads.py b/telethon/client/uploads.py index c2e08fde..4c9f9d32 100644 --- a/telethon/client/uploads.py +++ b/telethon/client/uploads.py @@ -116,6 +116,7 @@ class UploadMethods: supports_streaming: bool = False, schedule: 'hints.DateLike' = None, comment_to: 'typing.Union[int, types.Message]' = None, + ttl: int = None, **kwargs) -> 'types.Message': """ Sends message with the given file to the specified entity. @@ -273,6 +274,18 @@ class UploadMethods: This parameter takes precedence over ``reply_to``. If there is no linked chat, `telethon.errors.sgIdInvalidError` is raised. + ttl (`int`. optional): + The Time-To-Live of the file (also known as "self-destruct timer" + or "self-destructing media"). If set, files can only be viewed for + a short period of time before they disappear from the message + history automatically. + + The value must be at least 1 second, and at most 60 seconds, + otherwise Telegram will ignore this parameter. + + Not all types of media can be used with this parameter, such + as text documents, which will fail with ``TtlMediaInvalidError``. + Returns The `Message ` (or messages) containing the sent file, or messages if a list of them was passed. @@ -382,7 +395,7 @@ class UploadMethods: progress_callback=progress_callback, attributes=attributes, allow_cache=allow_cache, thumb=thumb, voice_note=voice_note, video_note=video_note, - supports_streaming=supports_streaming + supports_streaming=supports_streaming, ttl=ttl ) # e.g. invalid cast from :tl:`MessageMediaWebPage` @@ -402,7 +415,7 @@ class UploadMethods: progress_callback=None, reply_to=None, parse_mode=(), silent=None, schedule=None, supports_streaming=None, clear_draft=None, - force_document=False, background=None): + force_document=False, background=None, ttl=None): """Specialized version of .send_file for albums""" # We don't care if the user wants to avoid cache, we will use it # anyway. Why? The cached version will be exactly the same thing @@ -432,7 +445,7 @@ class UploadMethods: # it as media and then convert that to :tl:`InputMediaPhoto`. fh, fm, _ = await self._file_to_media( file, supports_streaming=supports_streaming, - force_document=force_document) + force_document=force_document, ttl=ttl) if isinstance(fm, (types.InputMediaUploadedPhoto, types.InputMediaPhotoExternal)): r = await self(functions.messages.UploadMediaRequest( entity, media=fm @@ -654,7 +667,8 @@ class UploadMethods: self, file, force_document=False, file_size=None, progress_callback=None, attributes=None, thumb=None, allow_cache=True, voice_note=False, video_note=False, - supports_streaming=False, mime_type=None, as_image=None): + supports_streaming=False, mime_type=None, as_image=None, + ttl=None): if not file: return None, None, None @@ -683,7 +697,8 @@ class UploadMethods: force_document=force_document, voice_note=voice_note, video_note=video_note, - supports_streaming=supports_streaming + supports_streaming=supports_streaming, + ttl=ttl ), as_image) except TypeError: # Can't turn whatever was given into media @@ -702,13 +717,13 @@ class UploadMethods: ) elif re.match('https?://', file): if as_image: - media = types.InputMediaPhotoExternal(file) + media = types.InputMediaPhotoExternal(file, ttl_seconds=ttl) else: - media = types.InputMediaDocumentExternal(file) + media = types.InputMediaDocumentExternal(file, ttl_seconds=ttl) else: bot_file = utils.resolve_bot_file_id(file) if bot_file: - media = utils.get_input_media(bot_file) + media = utils.get_input_media(bot_file, ttl=ttl) if media: pass # Already have media, don't check the rest @@ -718,7 +733,7 @@ class UploadMethods: 'an HTTP URL or a valid bot-API-like file ID'.format(file) ) elif as_image: - media = types.InputMediaUploadedPhoto(file_handle) + media = types.InputMediaUploadedPhoto(file_handle, ttl_seconds=ttl) else: attributes, mime_type = utils.get_attributes( file, @@ -743,7 +758,8 @@ class UploadMethods: mime_type=mime_type, attributes=attributes, thumb=thumb, - force_file=force_document and not is_image + force_file=force_document and not is_image, + ttl_seconds=ttl ) return file_handle, media, as_image diff --git a/telethon/utils.py b/telethon/utils.py index 6045506e..e8c59c01 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -429,7 +429,8 @@ def get_input_geo(geo): def get_input_media( media, *, is_photo=False, attributes=None, force_document=False, - voice_note=False, video_note=False, supports_streaming=False + voice_note=False, video_note=False, supports_streaming=False, + ttl=None ): """ Similar to :meth:`get_input_peer`, but for media. @@ -442,37 +443,39 @@ def get_input_media( if media.SUBCLASS_OF_ID == 0xfaf846f4: # crc32(b'InputMedia') return media elif media.SUBCLASS_OF_ID == 0x846363e0: # crc32(b'InputPhoto') - return types.InputMediaPhoto(media) + return types.InputMediaPhoto(media, ttl_seconds=ttl) elif media.SUBCLASS_OF_ID == 0xf33fdb68: # crc32(b'InputDocument') - return types.InputMediaDocument(media) + return types.InputMediaDocument(media, ttl_seconds=ttl) except AttributeError: _raise_cast_fail(media, 'InputMedia') if isinstance(media, types.MessageMediaPhoto): return types.InputMediaPhoto( id=get_input_photo(media.photo), - ttl_seconds=media.ttl_seconds + ttl_seconds=ttl or media.ttl_seconds ) if isinstance(media, (types.Photo, types.photos.Photo, types.PhotoEmpty)): return types.InputMediaPhoto( - id=get_input_photo(media) + id=get_input_photo(media), + ttl_seconds=ttl ) if isinstance(media, types.MessageMediaDocument): return types.InputMediaDocument( id=get_input_document(media.document), - ttl_seconds=media.ttl_seconds + ttl_seconds=ttl or media.ttl_seconds ) if isinstance(media, (types.Document, types.DocumentEmpty)): return types.InputMediaDocument( - id=get_input_document(media) + id=get_input_document(media), + ttl_seconds=ttl ) if isinstance(media, (types.InputFile, types.InputFileBig)): if is_photo: - return types.InputMediaUploadedPhoto(file=media) + return types.InputMediaUploadedPhoto(file=media, ttl_seconds=ttl) else: attrs, mime = get_attributes( media, @@ -483,7 +486,8 @@ def get_input_media( supports_streaming=supports_streaming ) return types.InputMediaUploadedDocument( - file=media, mime_type=mime, attributes=attrs, force_file=force_document) + file=media, mime_type=mime, attributes=attrs, force_file=force_document, + ttl_seconds=ttl) if isinstance(media, types.MessageMediaGame): return types.InputMediaGame(id=types.InputGameID( @@ -522,7 +526,7 @@ def get_input_media( return types.InputMediaEmpty() if isinstance(media, types.Message): - return get_input_media(media.media, is_photo=is_photo) + return get_input_media(media.media, is_photo=is_photo, ttl=ttl) if isinstance(media, types.MessageMediaPoll): if media.poll.quiz: From af201c12ba4ab27356819f143eadde94e435adab Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 11 Sep 2021 12:00:28 +0200 Subject: [PATCH 032/256] Begin writing a migration guide for V2 --- readthedocs/index.rst | 1 + readthedocs/misc/v2-migration-guide.rst | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 readthedocs/misc/v2-migration-guide.rst diff --git a/readthedocs/index.rst b/readthedocs/index.rst index f4b1d877..827823cd 100644 --- a/readthedocs/index.rst +++ b/readthedocs/index.rst @@ -103,6 +103,7 @@ You can also use the menu on the left to quickly skip over sections. :caption: Miscellaneous misc/changelog + misc/v2-migration-guide.rst misc/wall-of-shame.rst misc/compatibility-and-convenience diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst new file mode 100644 index 00000000..fc1914be --- /dev/null +++ b/readthedocs/misc/v2-migration-guide.rst @@ -0,0 +1,24 @@ +========================= +Version 2 Migration Guide +========================= + +Version 2 represents the second major version change, breaking compatibility +with old code beyond the usual raw API changes in order to clean up a lot of +the technical debt that has grown on the project. + +This document documents all the things you should be aware of when migrating +from Telethon version 1.x to 2.0 onwards. + + +User, chat and channel identifiers are now 64-bit numbers +--------------------------------------------------------- + +`Layer 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. + +For the full list of types changed, please review the above link. From f639992baaa5f8d97c17298eef5096b0c5ce805f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 11 Sep 2021 13:33:27 +0200 Subject: [PATCH 033/256] 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. --- readthedocs/misc/v2-migration-guide.rst | 27 +- telethon/__init__.py | 2 +- telethon/client/account.py | 165 +- telethon/client/auth.py | 1041 +++--- telethon/client/bots.py | 82 +- telethon/client/buttons.py | 131 +- telethon/client/chats.py | 1261 ++------ telethon/client/dialogs.py | 564 +--- telethon/client/downloads.py | 1508 +++++---- telethon/client/messageparse.py | 368 ++- telethon/client/messages.py | 1475 +++------ telethon/client/telegrambaseclient.py | 1240 +++----- telethon/client/telegramclient.py | 3850 ++++++++++++++++++++++- telethon/client/updates.py | 1060 ++++--- telethon/client/uploads.py | 964 ++---- telethon/client/users.py | 1065 ++++--- 16 files changed, 7971 insertions(+), 6832 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index fc1914be..cd887251 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -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 `__ 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 `__ 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 \ No newline at end of file diff --git a/telethon/__init__.py b/telethon/__init__.py index 3a62f1c8..d4e4c2c8 100644 --- a/telethon/__init__.py +++ b/telethon/__init__.py @@ -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 diff --git a/telethon/client/account.py b/telethon/client/account.py index d82235b6..6300b791 100644 --- a/telethon/client/account.py +++ b/telethon/client/account.py @@ -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 - `. + 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 - `. - - 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 diff --git a/telethon/client/auth.py b/telethon/client/auth.py index 9665262b..c27f6512 100644 --- a/telethon/client/auth.py +++ b/telethon/client/auth.py @@ -12,710 +12,407 @@ if typing.TYPE_CHECKING: from .telegramclient import TelegramClient -class AuthMethods: - - # region Public methods - - def start( - self: 'TelegramClient', - phone: typing.Callable[[], str] = lambda: input('Please enter your phone (or bot token): '), - password: typing.Callable[[], str] = lambda: getpass.getpass('Please enter your password: '), - *, - bot_token: str = None, - force_sms: bool = False, - code_callback: typing.Callable[[], typing.Union[str, int]] = None, - first_name: str = 'New User', - last_name: str = '', - max_attempts: int = 3) -> 'TelegramClient': - """ - Starts the client (connects and logs in if necessary). - - By default, this method will be interactive (asking for - user input if needed), and will handle 2FA if enabled too. - - If the phone doesn't belong to an existing account (and will hence - `sign_up` for a new one), **you are agreeing to Telegram's - Terms of Service. This is required and your account - will be banned otherwise.** See https://telegram.org/tos - and https://core.telegram.org/api/terms. - - If the event loop is already running, this method returns a - coroutine that you should await on your own code; otherwise - the loop is ran until said coroutine completes. - - Arguments - phone (`str` | `int` | `callable`): - The phone (or callable without arguments to get it) - to which the code will be sent. If a bot-token-like - string is given, it will be used as such instead. - The argument may be a coroutine. - - password (`str`, `callable`, optional): - The password for 2 Factor Authentication (2FA). - This is only required if it is enabled in your account. - The argument may be a coroutine. - - bot_token (`str`): - Bot Token obtained by `@BotFather `_ - to log in as a bot. Cannot be specified with ``phone`` (only - one of either allowed). - - force_sms (`bool`, optional): - Whether to force sending the code request as SMS. - This only makes sense when signing in with a `phone`. - - code_callback (`callable`, optional): - A callable that will be used to retrieve the Telegram - login code. Defaults to `input()`. - The argument may be a coroutine. - - first_name (`str`, optional): - The first name to be used if signing up. This has no - effect if the account already exists and you sign in. - - last_name (`str`, optional): - Similar to the first name, but for the last. Optional. - - max_attempts (`int`, optional): - How many times the code/password callback should be - retried or switching between signing in and signing up. - - Returns - This `TelegramClient`, so initialization - can be chained with ``.start()``. - - Example - .. code-block:: python - - client = TelegramClient('anon', api_id, api_hash) - - # Starting as a bot account - await client.start(bot_token=bot_token) - - # Starting as a user account - await client.start(phone) - # Please enter the code you received: 12345 - # Please enter your password: ******* - # (You are now logged in) - - # Starting using a context manager (this calls start()): - with client: - pass - """ - if code_callback is None: - def code_callback(): - return input('Please enter the code you received: ') - elif not callable(code_callback): - raise ValueError( - 'The code_callback parameter needs to be a callable ' - 'function that returns the code you received by Telegram.' - ) - - if not phone and not bot_token: - raise ValueError('No phone number or bot token provided.') - - if phone and bot_token and not callable(phone): - raise ValueError('Both a phone and a bot token provided, ' - 'must only provide one of either') - - coro = self._start( - phone=phone, - password=password, - bot_token=bot_token, - force_sms=force_sms, - code_callback=code_callback, - first_name=first_name, - last_name=last_name, - max_attempts=max_attempts - ) - return ( - coro if self.loop.is_running() - else self.loop.run_until_complete(coro) +def start( + self: 'TelegramClient', + phone: typing.Callable[[], str] = lambda: input('Please enter your phone (or bot token): '), + password: typing.Callable[[], str] = lambda: getpass.getpass('Please enter your password: '), + *, + bot_token: str = None, + force_sms: bool = False, + code_callback: typing.Callable[[], typing.Union[str, int]] = None, + first_name: str = 'New User', + last_name: str = '', + max_attempts: int = 3) -> 'TelegramClient': + if code_callback is None: + def code_callback(): + return input('Please enter the code you received: ') + elif not callable(code_callback): + raise ValueError( + 'The code_callback parameter needs to be a callable ' + 'function that returns the code you received by Telegram.' ) - async def _start( - self: 'TelegramClient', phone, password, bot_token, force_sms, - code_callback, first_name, last_name, max_attempts): - if not self.is_connected(): - await self.connect() + if not phone and not bot_token: + raise ValueError('No phone number or bot token provided.') - # Rather than using `is_user_authorized`, use `get_me`. While this is - # more expensive and needs to retrieve more data from the server, it - # enables the library to warn users trying to login to a different - # account. See #1172. - me = await self.get_me() - if me is not None: - # The warnings here are on a best-effort and may fail. - if bot_token: - # bot_token's first part has the bot ID, but it may be invalid - # so don't try to parse as int (instead cast our ID to string). - if bot_token[:bot_token.find(':')] != str(me.id): - warnings.warn( - 'the session already had an authorized user so it did ' - 'not login to the bot account using the provided ' - 'bot_token (it may not be using the user you expect)' - ) - elif phone and not callable(phone) and utils.parse_phone(phone) != me.phone: + if phone and bot_token and not callable(phone): + raise ValueError('Both a phone and a bot token provided, ' + 'must only provide one of either') + + coro = self._start( + phone=phone, + password=password, + bot_token=bot_token, + force_sms=force_sms, + code_callback=code_callback, + first_name=first_name, + last_name=last_name, + max_attempts=max_attempts + ) + return ( + coro if self.loop.is_running() + else self.loop.run_until_complete(coro) + ) + +async def _start( + self: 'TelegramClient', phone, password, bot_token, force_sms, + code_callback, first_name, last_name, max_attempts): + if not self.is_connected(): + await self.connect() + + # Rather than using `is_user_authorized`, use `get_me`. While this is + # more expensive and needs to retrieve more data from the server, it + # enables the library to warn users trying to login to a different + # account. See #1172. + me = await self.get_me() + if me is not None: + # The warnings here are on a best-effort and may fail. + if bot_token: + # bot_token's first part has the bot ID, but it may be invalid + # so don't try to parse as int (instead cast our ID to string). + if bot_token[:bot_token.find(':')] != str(me.id): warnings.warn( 'the session already had an authorized user so it did ' - 'not login to the user account using the provided ' - 'phone (it may not be using the user you expect)' + 'not login to the bot account using the provided ' + 'bot_token (it may not be using the user you expect)' ) - - return self - - if not bot_token: - # Turn the callable into a valid phone number (or bot token) - while callable(phone): - value = phone() - if inspect.isawaitable(value): - value = await value - - if ':' in value: - # Bot tokens have 'user_id:access_hash' format - bot_token = value - break - - phone = utils.parse_phone(value) or phone - - if bot_token: - await self.sign_in(bot_token=bot_token) - return self - - me = None - attempts = 0 - two_step_detected = False - - await self.send_code_request(phone, force_sms=force_sms) - sign_up = False # assume login - while attempts < max_attempts: - try: - value = code_callback() - if inspect.isawaitable(value): - value = await value - - # Since sign-in with no code works (it sends the code) - # we must double-check that here. Else we'll assume we - # logged in, and it will return None as the User. - if not value: - raise errors.PhoneCodeEmptyError(request=None) - - if sign_up: - me = await self.sign_up(value, first_name, last_name) - else: - # Raises SessionPasswordNeededError if 2FA enabled - me = await self.sign_in(phone, code=value) - break - except errors.SessionPasswordNeededError: - two_step_detected = True - break - except errors.PhoneNumberOccupiedError: - sign_up = False - except errors.PhoneNumberUnoccupiedError: - sign_up = True - except (errors.PhoneCodeEmptyError, - errors.PhoneCodeExpiredError, - errors.PhoneCodeHashEmptyError, - errors.PhoneCodeInvalidError): - print('Invalid code. Please try again.', file=sys.stderr) - - attempts += 1 - else: - raise RuntimeError( - '{} consecutive sign-in attempts failed. Aborting' - .format(max_attempts) + elif phone and not callable(phone) and utils.parse_phone(phone) != me.phone: + warnings.warn( + 'the session already had an authorized user so it did ' + 'not login to the user account using the provided ' + 'phone (it may not be using the user you expect)' ) - if two_step_detected: - if not password: - raise ValueError( - "Two-step verification is enabled for this account. " - "Please provide the 'password' argument to 'start()'." - ) - - if callable(password): - for _ in range(max_attempts): - try: - value = password() - if inspect.isawaitable(value): - value = await value - - me = await self.sign_in(phone=phone, password=value) - break - except errors.PasswordHashInvalidError: - print('Invalid password. Please try again', - file=sys.stderr) - else: - raise errors.PasswordHashInvalidError(request=None) - else: - me = await self.sign_in(phone=phone, password=password) - - # We won't reach here if any step failed (exit by exception) - signed, name = 'Signed in successfully as', utils.get_display_name(me) - try: - print(signed, name) - except UnicodeEncodeError: - # Some terminals don't support certain characters - print(signed, name.encode('utf-8', errors='ignore') - .decode('ascii', errors='ignore')) - return self - def _parse_phone_and_hash(self, phone, phone_hash): - """ - Helper method to both parse and validate phone and its hash. - """ - phone = utils.parse_phone(phone) or self._phone - if not phone: - raise ValueError( - 'Please make sure to call send_code_request first.' - ) + if not bot_token: + # Turn the callable into a valid phone number (or bot token) + while callable(phone): + value = phone() + if inspect.isawaitable(value): + value = await value - phone_hash = phone_hash or self._phone_code_hash.get(phone, None) - if not phone_hash: - raise ValueError('You also need to provide a phone_code_hash.') + if ':' in value: + # Bot tokens have 'user_id:access_hash' format + bot_token = value + break - return phone, phone_hash + phone = utils.parse_phone(value) or phone - async def sign_in( - self: 'TelegramClient', - phone: str = None, - code: typing.Union[str, int] = None, - *, - password: str = None, - bot_token: str = None, - phone_code_hash: str = None) -> 'typing.Union[types.User, types.auth.SentCode]': - """ - Logs in to Telegram to an existing user or bot account. + if bot_token: + await self.sign_in(bot_token=bot_token) + return self - You should only use this if you are not authorized yet. + me = None + attempts = 0 + two_step_detected = False - This method will send the code if it's not provided. + await self.send_code_request(phone, force_sms=force_sms) + sign_up = False # assume login + while attempts < max_attempts: + try: + value = code_callback() + if inspect.isawaitable(value): + value = await value - .. note:: + # Since sign-in with no code works (it sends the code) + # we must double-check that here. Else we'll assume we + # logged in, and it will return None as the User. + if not value: + raise errors.PhoneCodeEmptyError(request=None) - In most cases, you should simply use `start()` and not this method. - - Arguments - phone (`str` | `int`): - The phone to send the code to if no code was provided, - or to override the phone that was previously used with - these requests. - - code (`str` | `int`): - The code that Telegram sent. Note that if you have sent this - code through the application itself it will immediately - expire. If you want to send the code, obfuscate it somehow. - If you're not doing any of this you can ignore this note. - - password (`str`): - 2FA password, should be used if a previous call raised - ``SessionPasswordNeededError``. - - bot_token (`str`): - Used to sign in as a bot. Not all requests will be available. - This should be the hash the `@BotFather `_ - gave you. - - phone_code_hash (`str`, optional): - The hash returned by `send_code_request`. This can be left as - `None` to use the last hash known for the phone to be used. - - Returns - The signed in user, or the information about - :meth:`send_code_request`. - - Example - .. code-block:: python - - phone = '+34 123 123 123' - await client.sign_in(phone) # send code - - code = input('enter code: ') - await client.sign_in(phone, code) - """ - me = await self.get_me() - if me: - return me - - if phone and not code and not password: - return await self.send_code_request(phone) - elif code: - phone, phone_code_hash = \ - self._parse_phone_and_hash(phone, phone_code_hash) - - # May raise PhoneCodeEmptyError, PhoneCodeExpiredError, - # PhoneCodeHashEmptyError or PhoneCodeInvalidError. - request = functions.auth.SignInRequest( - phone, phone_code_hash, str(code) - ) - elif password: - pwd = await self(functions.account.GetPasswordRequest()) - request = functions.auth.CheckPasswordRequest( - pwd_mod.compute_check(pwd, password) - ) - elif bot_token: - request = functions.auth.ImportBotAuthorizationRequest( - flags=0, bot_auth_token=bot_token, - api_id=self.api_id, api_hash=self.api_hash - ) - else: - raise ValueError( - 'You must provide a phone and a code the first time, ' - 'and a password only if an RPCError was raised before.' - ) - - result = await self(request) - if isinstance(result, types.auth.AuthorizationSignUpRequired): - # Emulate pre-layer 104 behaviour - self._tos = result.terms_of_service - raise errors.PhoneNumberUnoccupiedError(request=request) - - return self._on_login(result.user) - - async def sign_up( - self: 'TelegramClient', - code: typing.Union[str, int], - first_name: str, - last_name: str = '', - *, - phone: str = None, - phone_code_hash: str = None) -> 'types.User': - """ - Signs up to Telegram as a new user account. - - Use this if you don't have an account yet. - - You must call `send_code_request` first. - - **By using this method you're agreeing to Telegram's - Terms of Service. This is required and your account - will be banned otherwise.** See https://telegram.org/tos - and https://core.telegram.org/api/terms. - - Arguments - code (`str` | `int`): - The code sent by Telegram - - first_name (`str`): - The first name to be used by the new account. - - last_name (`str`, optional) - Optional last name. - - phone (`str` | `int`, optional): - The phone to sign up. This will be the last phone used by - default (you normally don't need to set this). - - phone_code_hash (`str`, optional): - The hash returned by `send_code_request`. This can be left as - `None` to use the last hash known for the phone to be used. - - Returns - The new created :tl:`User`. - - Example - .. code-block:: python - - phone = '+34 123 123 123' - await client.send_code_request(phone) - - code = input('enter code: ') - await client.sign_up(code, first_name='Anna', last_name='Banana') - """ - me = await self.get_me() - if me: - return me - - # To prevent abuse, one has to try to sign in before signing up. This - # is the current way in which Telegram validates the code to sign up. - # - # `sign_in` will set `_tos`, so if it's set we don't need to call it - # because the user already tried to sign in. - # - # We're emulating pre-layer 104 behaviour so except the right error: - if not self._tos: - try: - return await self.sign_in( - phone=phone, - code=code, - phone_code_hash=phone_code_hash, - ) - except errors.PhoneNumberUnoccupiedError: - pass # code is correct and was used, now need to sign in - - if self._tos and self._tos.text: - if self.parse_mode: - t = self.parse_mode.unparse(self._tos.text, self._tos.entities) + if sign_up: + me = await self.sign_up(value, first_name, last_name) else: - t = self._tos.text - sys.stderr.write("{}\n".format(t)) - sys.stderr.flush() + # Raises SessionPasswordNeededError if 2FA enabled + me = await self.sign_in(phone, code=value) + break + except errors.SessionPasswordNeededError: + two_step_detected = True + break + except errors.PhoneNumberOccupiedError: + sign_up = False + except errors.PhoneNumberUnoccupiedError: + sign_up = True + except (errors.PhoneCodeEmptyError, + errors.PhoneCodeExpiredError, + errors.PhoneCodeHashEmptyError, + errors.PhoneCodeInvalidError): + print('Invalid code. Please try again.', file=sys.stderr) + attempts += 1 + else: + raise RuntimeError( + '{} consecutive sign-in attempts failed. Aborting' + .format(max_attempts) + ) + + if two_step_detected: + if not password: + raise ValueError( + "Two-step verification is enabled for this account. " + "Please provide the 'password' argument to 'start()'." + ) + + if callable(password): + for _ in range(max_attempts): + try: + value = password() + if inspect.isawaitable(value): + value = await value + + me = await self.sign_in(phone=phone, password=value) + break + except errors.PasswordHashInvalidError: + print('Invalid password. Please try again', + file=sys.stderr) + else: + raise errors.PasswordHashInvalidError(request=None) + else: + me = await self.sign_in(phone=phone, password=password) + + # We won't reach here if any step failed (exit by exception) + signed, name = 'Signed in successfully as', utils.get_display_name(me) + try: + print(signed, name) + except UnicodeEncodeError: + # Some terminals don't support certain characters + print(signed, name.encode('utf-8', errors='ignore') + .decode('ascii', errors='ignore')) + + return self + +def _parse_phone_and_hash(self, phone, phone_hash): + """ + Helper method to both parse and validate phone and its hash. + """ + phone = utils.parse_phone(phone) or self._phone + if not phone: + raise ValueError( + 'Please make sure to call send_code_request first.' + ) + + phone_hash = phone_hash or self._phone_code_hash.get(phone, None) + if not phone_hash: + raise ValueError('You also need to provide a phone_code_hash.') + + return phone, phone_hash + +async def sign_in( + self: 'TelegramClient', + phone: str = None, + code: typing.Union[str, int] = None, + *, + password: str = None, + bot_token: str = None, + phone_code_hash: str = None) -> 'typing.Union[types.User, types.auth.SentCode]': + me = await self.get_me() + if me: + return me + + if phone and not code and not password: + return await self.send_code_request(phone) + elif code: phone, phone_code_hash = \ self._parse_phone_and_hash(phone, phone_code_hash) - result = await self(functions.auth.SignUpRequest( - phone_number=phone, - phone_code_hash=phone_code_hash, - first_name=first_name, - last_name=last_name - )) - - if self._tos: - await self( - functions.help.AcceptTermsOfServiceRequest(self._tos.id)) - - return self._on_login(result.user) - - def _on_login(self, user): - """ - Callback called whenever the login or sign up process completes. - - Returns the input user parameter. - """ - self._bot = bool(user.bot) - self._self_input_peer = utils.get_input_peer(user, allow_self=False) - self._authorized = True - - return user - - async def send_code_request( - self: 'TelegramClient', - phone: str, - *, - force_sms: bool = False) -> 'types.auth.SentCode': - """ - Sends the Telegram code needed to login to the given phone number. - - Arguments - phone (`str` | `int`): - The phone to which the code will be sent. - - force_sms (`bool`, optional): - Whether to force sending as SMS. - - Returns - An instance of :tl:`SentCode`. - - Example - .. code-block:: python - - phone = '+34 123 123 123' - sent = await client.send_code_request(phone) - print(sent) - """ - result = None - phone = utils.parse_phone(phone) or self._phone - phone_hash = self._phone_code_hash.get(phone) - - if not phone_hash: - try: - result = await self(functions.auth.SendCodeRequest( - phone, self.api_id, self.api_hash, types.CodeSettings())) - except errors.AuthRestartError: - return await self.send_code_request(phone, force_sms=force_sms) - - # If we already sent a SMS, do not resend the code (hash may be empty) - if isinstance(result.type, types.auth.SentCodeTypeSms): - force_sms = False - - # phone_code_hash may be empty, if it is, do not save it (#1283) - if result.phone_code_hash: - self._phone_code_hash[phone] = phone_hash = result.phone_code_hash - else: - force_sms = True - - self._phone = phone - - if force_sms: - result = await self( - functions.auth.ResendCodeRequest(phone, phone_hash)) - - self._phone_code_hash[phone] = result.phone_code_hash - - return result - - async def qr_login(self: 'TelegramClient', ignored_ids: typing.List[int] = None) -> custom.QRLogin: - """ - Initiates the QR login procedure. - - Note that you must be connected before invoking this, as with any - other request. - - It is up to the caller to decide how to present the code to the user, - whether it's the URL, using the token bytes directly, or generating - a QR code and displaying it by other means. - - See the documentation for `QRLogin` to see how to proceed after this. - - Arguments - ignored_ids (List[`int`]): - List of already logged-in user IDs, to prevent logging in - twice with the same user. - - Returns - An instance of `QRLogin`. - - Example - .. code-block:: python - - def display_url_as_qr(url): - pass # do whatever to show url as a qr to the user - - qr_login = await client.qr_login() - display_url_as_qr(qr_login.url) - - # Important! You need to wait for the login to complete! - await qr_login.wait() - """ - qr_login = custom.QRLogin(self, ignored_ids or []) - await qr_login.recreate() - return qr_login - - async def log_out(self: 'TelegramClient') -> bool: - """ - Logs out Telegram and deletes the current ``*.session`` file. - - Returns - `True` if the operation was successful. - - Example - .. code-block:: python - - # Note: you will need to login again! - await client.log_out() - """ - try: - await self(functions.auth.LogOutRequest()) - except errors.RPCError: - return False - - self._bot = None - self._self_input_peer = None - self._authorized = False - self._state_cache.reset() - - await self.disconnect() - self.session.delete() - return True - - async def edit_2fa( - self: 'TelegramClient', - current_password: str = None, - new_password: str = None, - *, - hint: str = '', - email: str = None, - email_code_callback: typing.Callable[[int], str] = None) -> bool: - """ - Changes the 2FA settings of the logged in user. - - Review carefully the parameter explanations before using this method. - - Note that this method may be *incredibly* slow depending on the - prime numbers that must be used during the process to make sure - that everything is safe. - - Has no effect if both current and new password are omitted. - - Arguments - current_password (`str`, optional): - The current password, to authorize changing to ``new_password``. - Must be set if changing existing 2FA settings. - Must **not** be set if 2FA is currently disabled. - Passing this by itself will remove 2FA (if correct). - - new_password (`str`, optional): - The password to set as 2FA. - If 2FA was already enabled, ``current_password`` **must** be set. - Leaving this blank or `None` will remove the password. - - hint (`str`, optional): - Hint to be displayed by Telegram when it asks for 2FA. - Leaving unspecified is highly discouraged. - Has no effect if ``new_password`` is not set. - - email (`str`, optional): - Recovery and verification email. If present, you must also - set `email_code_callback`, else it raises ``ValueError``. - - email_code_callback (`callable`, optional): - If an email is provided, a callback that returns the code sent - to it must also be set. This callback may be asynchronous. - It should return a string with the code. The length of the - code will be passed to the callback as an input parameter. - - If the callback returns an invalid code, it will raise - ``CodeInvalidError``. - - Returns - `True` if successful, `False` otherwise. - - Example - .. code-block:: python - - # Setting a password for your account which didn't have - await client.edit_2fa(new_password='I_<3_Telethon') - - # Removing the password - await client.edit_2fa(current_password='I_<3_Telethon') - """ - if new_password is None and current_password is None: - return False - - if email and not callable(email_code_callback): - raise ValueError('email present without email_code_callback') - + # May raise PhoneCodeEmptyError, PhoneCodeExpiredError, + # PhoneCodeHashEmptyError or PhoneCodeInvalidError. + request = functions.auth.SignInRequest( + phone, phone_code_hash, str(code) + ) + elif password: pwd = await self(functions.account.GetPasswordRequest()) - pwd.new_algo.salt1 += os.urandom(32) - assert isinstance(pwd, types.account.Password) - if not pwd.has_password and current_password: - current_password = None + request = functions.auth.CheckPasswordRequest( + pwd_mod.compute_check(pwd, password) + ) + elif bot_token: + request = functions.auth.ImportBotAuthorizationRequest( + flags=0, bot_auth_token=bot_token, + api_id=self.api_id, api_hash=self.api_hash + ) + else: + raise ValueError( + 'You must provide a phone and a code the first time, ' + 'and a password only if an RPCError was raised before.' + ) - if current_password: - password = pwd_mod.compute_check(pwd, current_password) - else: - password = types.InputCheckPasswordEmpty() + result = await self(request) + if isinstance(result, types.auth.AuthorizationSignUpRequired): + # Emulate pre-layer 104 behaviour + self._tos = result.terms_of_service + raise errors.PhoneNumberUnoccupiedError(request=request) - if new_password: - new_password_hash = pwd_mod.compute_digest( - pwd.new_algo, new_password) - else: - new_password_hash = b'' + return self._on_login(result.user) +async def sign_up( + self: 'TelegramClient', + code: typing.Union[str, int], + first_name: str, + last_name: str = '', + *, + phone: str = None, + phone_code_hash: str = None) -> 'types.User': + me = await self.get_me() + if me: + return me + + # To prevent abuse, one has to try to sign in before signing up. This + # is the current way in which Telegram validates the code to sign up. + # + # `sign_in` will set `_tos`, so if it's set we don't need to call it + # because the user already tried to sign in. + # + # We're emulating pre-layer 104 behaviour so except the right error: + if not self._tos: try: - await self(functions.account.UpdatePasswordSettingsRequest( - password=password, - new_settings=types.account.PasswordInputSettings( - new_algo=pwd.new_algo, - new_password_hash=new_password_hash, - hint=hint, - email=email, - new_secure_settings=None - ) - )) - except errors.EmailUnconfirmedError as e: - code = email_code_callback(e.code_length) - if inspect.isawaitable(code): - code = await code + return await self.sign_in( + phone=phone, + code=code, + phone_code_hash=phone_code_hash, + ) + except errors.PhoneNumberUnoccupiedError: + pass # code is correct and was used, now need to sign in - code = str(code) - await self(functions.account.ConfirmPasswordEmailRequest(code)) + if self._tos and self._tos.text: + if self.parse_mode: + t = self.parse_mode.unparse(self._tos.text, self._tos.entities) + else: + t = self._tos.text + sys.stderr.write("{}\n".format(t)) + sys.stderr.flush() - return True + phone, phone_code_hash = \ + self._parse_phone_and_hash(phone, phone_code_hash) - # endregion + result = await self(functions.auth.SignUpRequest( + phone_number=phone, + phone_code_hash=phone_code_hash, + first_name=first_name, + last_name=last_name + )) - # region with blocks + if self._tos: + await self( + functions.help.AcceptTermsOfServiceRequest(self._tos.id)) - async def __aenter__(self): - return await self.start() + return self._on_login(result.user) - async def __aexit__(self, *args): - await self.disconnect() +def _on_login(self, user): + """ + Callback called whenever the login or sign up process completes. + Returns the input user parameter. + """ + self._bot = bool(user.bot) + self._self_input_peer = utils.get_input_peer(user, allow_self=False) + self._authorized = True - __enter__ = helpers._sync_enter - __exit__ = helpers._sync_exit + return user - # endregion +async def send_code_request( + self: 'TelegramClient', + phone: str, + *, + force_sms: bool = False) -> 'types.auth.SentCode': + result = None + phone = utils.parse_phone(phone) or self._phone + phone_hash = self._phone_code_hash.get(phone) + + if not phone_hash: + try: + result = await self(functions.auth.SendCodeRequest( + phone, self.api_id, self.api_hash, types.CodeSettings())) + except errors.AuthRestartError: + return await self.send_code_request(phone, force_sms=force_sms) + + # If we already sent a SMS, do not resend the code (hash may be empty) + if isinstance(result.type, types.auth.SentCodeTypeSms): + force_sms = False + + # phone_code_hash may be empty, if it is, do not save it (#1283) + if result.phone_code_hash: + self._phone_code_hash[phone] = phone_hash = result.phone_code_hash + else: + force_sms = True + + self._phone = phone + + if force_sms: + result = await self( + functions.auth.ResendCodeRequest(phone, phone_hash)) + + self._phone_code_hash[phone] = result.phone_code_hash + + return result + +async def qr_login(self: 'TelegramClient', ignored_ids: typing.List[int] = None) -> custom.QRLogin: + qr_login = custom.QRLogin(self, ignored_ids or []) + await qr_login.recreate() + return qr_login + +async def log_out(self: 'TelegramClient') -> bool: + try: + await self(functions.auth.LogOutRequest()) + except errors.RPCError: + return False + + self._bot = None + self._self_input_peer = None + self._authorized = False + self._state_cache.reset() + + await self.disconnect() + self.session.delete() + return True + +async def edit_2fa( + self: 'TelegramClient', + current_password: str = None, + new_password: str = None, + *, + hint: str = '', + email: str = None, + email_code_callback: typing.Callable[[int], str] = None) -> bool: + if new_password is None and current_password is None: + return False + + if email and not callable(email_code_callback): + raise ValueError('email present without email_code_callback') + + pwd = await self(functions.account.GetPasswordRequest()) + pwd.new_algo.salt1 += os.urandom(32) + assert isinstance(pwd, types.account.Password) + if not pwd.has_password and current_password: + current_password = None + + if current_password: + password = pwd_mod.compute_check(pwd, current_password) + else: + password = types.InputCheckPasswordEmpty() + + if new_password: + new_password_hash = pwd_mod.compute_digest( + pwd.new_algo, new_password) + else: + new_password_hash = b'' + + try: + await self(functions.account.UpdatePasswordSettingsRequest( + password=password, + new_settings=types.account.PasswordInputSettings( + new_algo=pwd.new_algo, + new_password_hash=new_password_hash, + hint=hint, + email=email, + new_secure_settings=None + ) + )) + except errors.EmailUnconfirmedError as e: + code = email_code_callback(e.code_length) + if inspect.isawaitable(code): + code = await code + + code = str(code) + await self(functions.account.ConfirmPasswordEmailRequest(code)) + + return True diff --git a/telethon/client/bots.py b/telethon/client/bots.py index 044d8513..0912fc20 100644 --- a/telethon/client/bots.py +++ b/telethon/client/bots.py @@ -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 - `. - - 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) diff --git a/telethon/client/buttons.py b/telethon/client/buttons.py index 7e848ab1..41413708 100644 --- a/telethon/client/buttons.py +++ b/telethon/client/buttons.py @@ -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) diff --git a/telethon/client/chats.py b/telethon/client/chats.py index dfbeddcc..0429d563 100644 --- a/telethon/client/chats.py +++ b/telethon/client/chats.py @@ -404,965 +404,370 @@ class _ProfilePhotoIter(RequestIter): self.request.offset_id = result.messages[-1].id -class ChatMethods: - - # region Public methods - - def iter_participants( - self: 'TelegramClient', - entity: 'hints.EntityLike', - limit: float = None, - *, - search: str = '', - filter: 'types.TypeChannelParticipantsFilter' = None, - aggressive: bool = False) -> _ParticipantsIter: - """ - Iterator over the participants belonging to the specified chat. - - The order is unspecified. - - Arguments - entity (`entity`): - The entity from which to retrieve the participants list. - - limit (`int`): - Limits amount of participants fetched. - - search (`str`, optional): - Look for participants with this string in name/username. - - If ``aggressive is True``, the symbols from this string will - be used. - - filter (:tl:`ChannelParticipantsFilter`, optional): - The filter to be used, if you want e.g. only admins - Note that you might not have permissions for some filter. - This has no effect for normal chats or users. - - .. note:: - - The filter :tl:`ChannelParticipantsBanned` will return - *restricted* users. If you want *banned* users you should - use :tl:`ChannelParticipantsKicked` instead. - - aggressive (`bool`, optional): - Aggressively looks for all participants in the chat. - - This is useful for channels since 20 July 2018, - Telegram added a server-side limit where only the - first 200 members can be retrieved. With this flag - set, more than 200 will be often be retrieved. - - This has no effect if a ``filter`` is given. - - Yields - The :tl:`User` objects returned by :tl:`GetParticipantsRequest` - with an additional ``.participant`` attribute which is the - matched :tl:`ChannelParticipant` type for channels/megagroups - or :tl:`ChatParticipants` for normal chats. - - Example - .. code-block:: python - - # Show all user IDs in a chat - async for user in client.iter_participants(chat): - print(user.id) - - # Search by name - async for user in client.iter_participants(chat, search='name'): - print(user.username) - - # Filter by admins - from telethon.tl.types import ChannelParticipantsAdmins - async for user in client.iter_participants(chat, filter=ChannelParticipantsAdmins): - print(user.first_name) - """ - return _ParticipantsIter( - self, - limit, - entity=entity, - filter=filter, - search=search, - aggressive=aggressive - ) - - async def get_participants( - self: 'TelegramClient', - *args, - **kwargs) -> 'hints.TotalList': - """ - Same as `iter_participants()`, but returns a - `TotalList ` instead. - - Example - .. code-block:: python - - users = await client.get_participants(chat) - print(users[0].first_name) - - for user in users: - if user.username is not None: - print(user.username) - """ - return await self.iter_participants(*args, **kwargs).collect() - - get_participants.__signature__ = inspect.signature(iter_participants) - - - def iter_admin_log( - self: 'TelegramClient', - entity: 'hints.EntityLike', - limit: float = None, - *, - max_id: int = 0, - min_id: int = 0, - search: str = None, - admins: 'hints.EntitiesLike' = None, - join: bool = None, - leave: bool = None, - invite: bool = None, - restrict: bool = None, - unrestrict: bool = None, - ban: bool = None, - unban: bool = None, - promote: bool = None, - demote: bool = None, - info: bool = None, - settings: bool = None, - pinned: bool = None, - edit: bool = None, - delete: bool = None, - group_call: bool = None) -> _AdminLogIter: - """ - Iterator over the admin log for the specified channel. - - The default order is from the most recent event to to the oldest. - - Note that you must be an administrator of it to use this method. - - If none of the filters are present (i.e. they all are `None`), - *all* event types will be returned. If at least one of them is - `True`, only those that are true will be returned. - - Arguments - entity (`entity`): - The channel entity from which to get its admin log. - - limit (`int` | `None`, optional): - Number of events to be retrieved. - - The limit may also be `None`, which would eventually return - the whole history. - - max_id (`int`): - All the events with a higher (newer) ID or equal to this will - be excluded. - - min_id (`int`): - All the events with a lower (older) ID or equal to this will - be excluded. - - search (`str`): - The string to be used as a search query. - - admins (`entity` | `list`): - If present, the events will be filtered by these admins - (or single admin) and only those caused by them will be - returned. - - join (`bool`): - If `True`, events for when a user joined will be returned. - - leave (`bool`): - If `True`, events for when a user leaves will be returned. - - invite (`bool`): - If `True`, events for when a user joins through an invite - link will be returned. - - restrict (`bool`): - If `True`, events with partial restrictions will be - returned. This is what the API calls "ban". - - unrestrict (`bool`): - If `True`, events removing restrictions will be returned. - This is what the API calls "unban". - - ban (`bool`): - If `True`, events applying or removing all restrictions will - be returned. This is what the API calls "kick" (restricting - all permissions removed is a ban, which kicks the user). - - unban (`bool`): - If `True`, events removing all restrictions will be - returned. This is what the API calls "unkick". - - promote (`bool`): - If `True`, events with admin promotions will be returned. - - demote (`bool`): - If `True`, events with admin demotions will be returned. - - info (`bool`): - If `True`, events changing the group info will be returned. - - settings (`bool`): - If `True`, events changing the group settings will be - returned. - - pinned (`bool`): - If `True`, events of new pinned messages will be returned. - - edit (`bool`): - If `True`, events of message edits will be returned. - - delete (`bool`): - If `True`, events of message deletions will be returned. - - group_call (`bool`): - If `True`, events related to group calls will be returned. - - Yields - Instances of `AdminLogEvent `. - - Example - .. code-block:: python - - async for event in client.iter_admin_log(channel): - if event.changed_title: - print('The title changed from', event.old, 'to', event.new) - """ - return _AdminLogIter( - self, - limit, - entity=entity, - admins=admins, - search=search, - min_id=min_id, - max_id=max_id, - join=join, - leave=leave, - invite=invite, - restrict=restrict, - unrestrict=unrestrict, - ban=ban, - unban=unban, - promote=promote, - demote=demote, - info=info, - settings=settings, - pinned=pinned, - edit=edit, - delete=delete, - group_call=group_call - ) - - async def get_admin_log( - self: 'TelegramClient', - *args, - **kwargs) -> 'hints.TotalList': - """ - Same as `iter_admin_log()`, but returns a ``list`` instead. - - Example - .. code-block:: python - - # Get a list of deleted message events which said "heck" - events = await client.get_admin_log(channel, search='heck', delete=True) - - # Print the old message before it was deleted - print(events[0].old) - """ - return await self.iter_admin_log(*args, **kwargs).collect() - - get_admin_log.__signature__ = inspect.signature(iter_admin_log) - - def iter_profile_photos( - self: 'TelegramClient', - entity: 'hints.EntityLike', - limit: int = None, - *, - offset: int = 0, - max_id: int = 0) -> _ProfilePhotoIter: - """ - Iterator over a user's profile photos or a chat's photos. - - The order is from the most recent photo to the oldest. - - Arguments - entity (`entity`): - The entity from which to get the profile or chat photos. - - limit (`int` | `None`, optional): - Number of photos to be retrieved. - - The limit may also be `None`, which would eventually all - the photos that are still available. - - offset (`int`): - How many photos should be skipped before returning the first one. - - max_id (`int`): - The maximum ID allowed when fetching photos. - - Yields - Instances of :tl:`Photo`. - - Example - .. code-block:: python - - # Download all the profile photos of some user - async for photo in client.iter_profile_photos(user): - await client.download_media(photo) - """ - return _ProfilePhotoIter( - self, - limit, - entity=entity, - offset=offset, - max_id=max_id - ) - - async def get_profile_photos( - self: 'TelegramClient', - *args, - **kwargs) -> 'hints.TotalList': - """ - Same as `iter_profile_photos()`, but returns a - `TotalList ` instead. - - Example - .. code-block:: python - - # Get the photos of a channel - photos = await client.get_profile_photos(channel) - - # Download the oldest photo - await client.download_media(photos[-1]) - """ - return await self.iter_profile_photos(*args, **kwargs).collect() - - get_profile_photos.__signature__ = inspect.signature(iter_profile_photos) - - def action( - self: 'TelegramClient', - entity: 'hints.EntityLike', - action: 'typing.Union[str, types.TypeSendMessageAction]', - *, - delay: float = 4, - auto_cancel: bool = True) -> 'typing.Union[_ChatAction, typing.Coroutine]': - """ - Returns a context-manager object to represent a "chat action". - - Chat actions indicate things like "user is typing", "user is - uploading a photo", etc. - - If the action is ``'cancel'``, you should just ``await`` the result, - since it makes no sense to use a context-manager for it. - - See the example below for intended usage. - - Arguments - entity (`entity`): - The entity where the action should be showed in. - - action (`str` | :tl:`SendMessageAction`): - The action to show. You can either pass a instance of - :tl:`SendMessageAction` or better, a string used while: - - * ``'typing'``: typing a text message. - * ``'contact'``: choosing a contact. - * ``'game'``: playing a game. - * ``'location'``: choosing a geo location. - * ``'sticker'``: choosing a sticker. - * ``'record-audio'``: recording a voice note. - You may use ``'record-voice'`` as alias. - * ``'record-round'``: recording a round video. - * ``'record-video'``: recording a normal video. - * ``'audio'``: sending an audio file (voice note or song). - You may use ``'voice'`` and ``'song'`` as aliases. - * ``'round'``: uploading a round video. - * ``'video'``: uploading a video file. - * ``'photo'``: uploading a photo. - * ``'document'``: uploading a document file. - You may use ``'file'`` as alias. - * ``'cancel'``: cancel any pending action in this chat. - - Invalid strings will raise a ``ValueError``. - - delay (`int` | `float`): - The delay, in seconds, to wait between sending actions. - For example, if the delay is 5 and it takes 7 seconds to - do something, three requests will be made at 0s, 5s, and - 7s to cancel the action. - - auto_cancel (`bool`): - Whether the action should be cancelled once the context - manager exists or not. The default is `True`, since - you don't want progress to be shown when it has already - completed. - - Returns - Either a context-manager object or a coroutine. - - Example - .. code-block:: python - - # Type for 2 seconds, then send a message - async with client.action(chat, 'typing'): - await asyncio.sleep(2) - await client.send_message(chat, 'Hello world! I type slow ^^') - - # Cancel any previous action - await client.action(chat, 'cancel') - - # Upload a document, showing its progress (most clients ignore this) - async with client.action(chat, 'document') as action: - await client.send_file(chat, zip_file, progress_callback=action.progress) - """ - if isinstance(action, str): - try: - action = _ChatAction._str_mapping[action.lower()] - except KeyError: - raise ValueError( - 'No such action "{}"'.format(action)) from None - elif not isinstance(action, types.TLObject) or action.SUBCLASS_OF_ID != 0x20b2cc21: - # 0x20b2cc21 = crc32(b'SendMessageAction') - if isinstance(action, type): - raise ValueError('You must pass an instance, not the class') - else: - raise ValueError('Cannot use {} as action'.format(action)) - - if isinstance(action, types.SendMessageCancelAction): - # ``SetTypingRequest.resolve`` will get input peer of ``entity``. - return self(functions.messages.SetTypingRequest( - entity, types.SendMessageCancelAction())) - - return _ChatAction( - self, entity, action, delay=delay, auto_cancel=auto_cancel) - - async def edit_admin( - self: 'TelegramClient', - entity: 'hints.EntityLike', - user: 'hints.EntityLike', - *, - change_info: bool = None, - post_messages: bool = None, - edit_messages: bool = None, - delete_messages: bool = None, - ban_users: bool = None, - invite_users: bool = None, - pin_messages: bool = None, - add_admins: bool = None, - manage_call: bool = None, - anonymous: bool = None, - is_admin: bool = None, - title: str = None) -> types.Updates: - """ - Edits admin permissions for someone in a chat. - - Raises an error if a wrong combination of rights are given - (e.g. you don't have enough permissions to grant one). - - Unless otherwise stated, permissions will work in channels and megagroups. - - Arguments - entity (`entity`): - The channel, megagroup or chat where the promotion should happen. - - user (`entity`): - The user to be promoted. - - change_info (`bool`, optional): - Whether the user will be able to change info. - - post_messages (`bool`, optional): - Whether the user will be able to post in the channel. - This will only work in broadcast channels. - - edit_messages (`bool`, optional): - Whether the user will be able to edit messages in the channel. - This will only work in broadcast channels. - - delete_messages (`bool`, optional): - Whether the user will be able to delete messages. - - ban_users (`bool`, optional): - Whether the user will be able to ban users. - - invite_users (`bool`, optional): - Whether the user will be able to invite users. Needs some testing. - - pin_messages (`bool`, optional): - Whether the user will be able to pin messages. - - add_admins (`bool`, optional): - Whether the user will be able to add admins. - - manage_call (`bool`, optional): - Whether the user will be able to manage group calls. - - anonymous (`bool`, optional): - Whether the user will remain anonymous when sending messages. - The sender of the anonymous messages becomes the group itself. - - .. note:: - - Users may be able to identify the anonymous admin by its - custom title, so additional care is needed when using both - ``anonymous`` and custom titles. For example, if multiple - anonymous admins share the same title, users won't be able - to distinguish them. - - is_admin (`bool`, optional): - Whether the user will be an admin in the chat. - This will only work in small group chats. - Whether the user will be an admin in the chat. This is the - only permission available in small group chats, and when - used in megagroups, all non-explicitly set permissions will - have this value. - - Essentially, only passing ``is_admin=True`` will grant all - permissions, but you can still disable those you need. - - title (`str`, optional): - The custom title (also known as "rank") to show for this admin. - This text will be shown instead of the "admin" badge. - This will only work in channels and megagroups. - - When left unspecified or empty, the default localized "admin" - badge will be shown. - - Returns - The resulting :tl:`Updates` object. - - Example - .. code-block:: python - - # Allowing `user` to pin messages in `chat` - await client.edit_admin(chat, user, pin_messages=True) - - # Granting all permissions except for `add_admins` - await client.edit_admin(chat, user, is_admin=True, add_admins=False) - """ - entity = await self.get_input_entity(entity) - user = await self.get_input_entity(user) - ty = helpers._entity_type(user) - if ty != helpers._EntityType.USER: - raise ValueError('You must pass a user entity') - - perm_names = ( - 'change_info', 'post_messages', 'edit_messages', 'delete_messages', - 'ban_users', 'invite_users', 'pin_messages', 'add_admins', - 'anonymous', 'manage_call', - ) - - ty = helpers._entity_type(entity) - if ty == helpers._EntityType.CHANNEL: - # If we try to set these permissions in a megagroup, we - # would get a RIGHT_FORBIDDEN. However, it makes sense - # that an admin can post messages, so we want to avoid the error - if post_messages or edit_messages: - # TODO get rid of this once sessions cache this information - if entity.channel_id not in self._megagroup_cache: - full_entity = await self.get_entity(entity) - self._megagroup_cache[entity.channel_id] = full_entity.megagroup - - if self._megagroup_cache[entity.channel_id]: - post_messages = None - edit_messages = None - - perms = locals() - return await self(functions.channels.EditAdminRequest(entity, user, types.ChatAdminRights(**{ - # A permission is its explicit (not-None) value or `is_admin`. - # This essentially makes `is_admin` be the default value. - name: perms[name] if perms[name] is not None else is_admin - for name in perm_names - }), rank=title or '')) - - elif ty == helpers._EntityType.CHAT: - # If the user passed any permission in a small - # group chat, they must be a full admin to have it. - if is_admin is None: - is_admin = any(locals()[x] for x in perm_names) - - return await self(functions.messages.EditChatAdminRequest( - entity, user, is_admin=is_admin)) - - else: +def iter_participants( + self: 'TelegramClient', + entity: 'hints.EntityLike', + limit: float = None, + *, + search: str = '', + filter: 'types.TypeChannelParticipantsFilter' = None, + aggressive: bool = False) -> _ParticipantsIter: + return _ParticipantsIter( + self, + limit, + entity=entity, + filter=filter, + search=search, + aggressive=aggressive + ) + +async def get_participants( + self: 'TelegramClient', + *args, + **kwargs) -> 'hints.TotalList': + return await self.iter_participants(*args, **kwargs).collect() + + +def iter_admin_log( + self: 'TelegramClient', + entity: 'hints.EntityLike', + limit: float = None, + *, + max_id: int = 0, + min_id: int = 0, + search: str = None, + admins: 'hints.EntitiesLike' = None, + join: bool = None, + leave: bool = None, + invite: bool = None, + restrict: bool = None, + unrestrict: bool = None, + ban: bool = None, + unban: bool = None, + promote: bool = None, + demote: bool = None, + info: bool = None, + settings: bool = None, + pinned: bool = None, + edit: bool = None, + delete: bool = None, + group_call: bool = None) -> _AdminLogIter: + return _AdminLogIter( + self, + limit, + entity=entity, + admins=admins, + search=search, + min_id=min_id, + max_id=max_id, + join=join, + leave=leave, + invite=invite, + restrict=restrict, + unrestrict=unrestrict, + ban=ban, + unban=unban, + promote=promote, + demote=demote, + info=info, + settings=settings, + pinned=pinned, + edit=edit, + delete=delete, + group_call=group_call + ) + +async def get_admin_log( + self: 'TelegramClient', + *args, + **kwargs) -> 'hints.TotalList': + return await self.iter_admin_log(*args, **kwargs).collect() + + +def iter_profile_photos( + self: 'TelegramClient', + entity: 'hints.EntityLike', + limit: int = None, + *, + offset: int = 0, + max_id: int = 0) -> _ProfilePhotoIter: + return _ProfilePhotoIter( + self, + limit, + entity=entity, + offset=offset, + max_id=max_id + ) + +async def get_profile_photos( + self: 'TelegramClient', + *args, + **kwargs) -> 'hints.TotalList': + return await self.iter_profile_photos(*args, **kwargs).collect() + + +def action( + self: 'TelegramClient', + entity: 'hints.EntityLike', + action: 'typing.Union[str, types.TypeSendMessageAction]', + *, + delay: float = 4, + auto_cancel: bool = True) -> 'typing.Union[_ChatAction, typing.Coroutine]': + if isinstance(action, str): + try: + action = _ChatAction._str_mapping[action.lower()] + except KeyError: raise ValueError( - 'You can only edit permissions in groups and channels') + 'No such action "{}"'.format(action)) from None + elif not isinstance(action, types.TLObject) or action.SUBCLASS_OF_ID != 0x20b2cc21: + # 0x20b2cc21 = crc32(b'SendMessageAction') + if isinstance(action, type): + raise ValueError('You must pass an instance, not the class') + else: + raise ValueError('Cannot use {} as action'.format(action)) - async def edit_permissions( - self: 'TelegramClient', - entity: 'hints.EntityLike', - user: 'typing.Optional[hints.EntityLike]' = None, - until_date: 'hints.DateLike' = None, - *, - view_messages: bool = True, - send_messages: bool = True, - send_media: bool = True, - send_stickers: bool = True, - send_gifs: bool = True, - send_games: bool = True, - send_inline: bool = True, - embed_link_previews: bool = True, - send_polls: bool = True, - change_info: bool = True, - invite_users: bool = True, - pin_messages: bool = True) -> types.Updates: - """ - Edits user restrictions in a chat. + if isinstance(action, types.SendMessageCancelAction): + # ``SetTypingRequest.resolve`` will get input peer of ``entity``. + return self(functions.messages.SetTypingRequest( + entity, types.SendMessageCancelAction())) - Set an argument to `False` to apply a restriction (i.e. remove - the permission), or omit them to use the default `True` (i.e. - don't apply a restriction). + return _ChatAction( + self, entity, action, delay=delay, auto_cancel=auto_cancel) - Raises an error if a wrong combination of rights are given - (e.g. you don't have enough permissions to revoke one). +async def edit_admin( + self: 'TelegramClient', + entity: 'hints.EntityLike', + user: 'hints.EntityLike', + *, + change_info: bool = None, + post_messages: bool = None, + edit_messages: bool = None, + delete_messages: bool = None, + ban_users: bool = None, + invite_users: bool = None, + pin_messages: bool = None, + add_admins: bool = None, + manage_call: bool = None, + anonymous: bool = None, + is_admin: bool = None, + title: str = None) -> types.Updates: + entity = await self.get_input_entity(entity) + user = await self.get_input_entity(user) + ty = helpers._entity_type(user) + if ty != helpers._EntityType.USER: + raise ValueError('You must pass a user entity') - By default, each boolean argument is `True`, meaning that it - is true that the user has access to the default permission - and may be able to make use of it. + perm_names = ( + 'change_info', 'post_messages', 'edit_messages', 'delete_messages', + 'ban_users', 'invite_users', 'pin_messages', 'add_admins', + 'anonymous', 'manage_call', + ) - If you set an argument to `False`, then a restriction is applied - regardless of the default permissions. + ty = helpers._entity_type(entity) + if ty == helpers._EntityType.CHANNEL: + # If we try to set these permissions in a megagroup, we + # would get a RIGHT_FORBIDDEN. However, it makes sense + # that an admin can post messages, so we want to avoid the error + if post_messages or edit_messages: + # TODO get rid of this once sessions cache this information + if entity.channel_id not in self._megagroup_cache: + full_entity = await self.get_entity(entity) + self._megagroup_cache[entity.channel_id] = full_entity.megagroup - It is important to note that `True` does *not* mean grant, only - "don't restrict", and this is where the default permissions come - in. A user may have not been revoked the ``pin_messages`` permission - (it is `True`) but they won't be able to use it if the default - permissions don't allow it either. + if self._megagroup_cache[entity.channel_id]: + post_messages = None + edit_messages = None - Arguments - entity (`entity`): - The channel or megagroup where the restriction should happen. + perms = locals() + return await self(functions.channels.EditAdminRequest(entity, user, types.ChatAdminRights(**{ + # A permission is its explicit (not-None) value or `is_admin`. + # This essentially makes `is_admin` be the default value. + name: perms[name] if perms[name] is not None else is_admin + for name in perm_names + }), rank=title or '')) - user (`entity`, optional): - If specified, the permission will be changed for the specific user. - If left as `None`, the default chat permissions will be updated. + elif ty == helpers._EntityType.CHAT: + # If the user passed any permission in a small + # group chat, they must be a full admin to have it. + if is_admin is None: + is_admin = any(locals()[x] for x in perm_names) - until_date (`DateLike`, optional): - When the user will be unbanned. + return await self(functions.messages.EditChatAdminRequest( + entity, user, is_admin=is_admin)) - If the due date or duration is longer than 366 days or shorter than - 30 seconds, the ban will be forever. Defaults to ``0`` (ban forever). + else: + raise ValueError( + 'You can only edit permissions in groups and channels') - view_messages (`bool`, optional): - Whether the user is able to view messages or not. - Forbidding someone from viewing messages equals to banning them. - This will only work if ``user`` is set. +async def edit_permissions( + self: 'TelegramClient', + entity: 'hints.EntityLike', + user: 'typing.Optional[hints.EntityLike]' = None, + until_date: 'hints.DateLike' = None, + *, + view_messages: bool = True, + send_messages: bool = True, + send_media: bool = True, + send_stickers: bool = True, + send_gifs: bool = True, + send_games: bool = True, + send_inline: bool = True, + embed_link_previews: bool = True, + send_polls: bool = True, + change_info: bool = True, + invite_users: bool = True, + pin_messages: bool = True) -> types.Updates: + entity = await self.get_input_entity(entity) + ty = helpers._entity_type(entity) + if ty != helpers._EntityType.CHANNEL: + raise ValueError('You must pass either a channel or a supergroup') - send_messages (`bool`, optional): - Whether the user is able to send messages or not. + rights = types.ChatBannedRights( + until_date=until_date, + view_messages=not view_messages, + send_messages=not send_messages, + send_media=not send_media, + send_stickers=not send_stickers, + send_gifs=not send_gifs, + send_games=not send_games, + send_inline=not send_inline, + embed_links=not embed_link_previews, + send_polls=not send_polls, + change_info=not change_info, + invite_users=not invite_users, + pin_messages=not pin_messages + ) - send_media (`bool`, optional): - Whether the user is able to send media or not. - - send_stickers (`bool`, optional): - Whether the user is able to send stickers or not. - - send_gifs (`bool`, optional): - Whether the user is able to send animated gifs or not. - - send_games (`bool`, optional): - Whether the user is able to send games or not. - - send_inline (`bool`, optional): - Whether the user is able to use inline bots or not. - - embed_link_previews (`bool`, optional): - Whether the user is able to enable the link preview in the - messages they send. Note that the user will still be able to - send messages with links if this permission is removed, but - these links won't display a link preview. - - send_polls (`bool`, optional): - Whether the user is able to send polls or not. - - change_info (`bool`, optional): - Whether the user is able to change info or not. - - invite_users (`bool`, optional): - Whether the user is able to invite other users or not. - - pin_messages (`bool`, optional): - Whether the user is able to pin messages or not. - - Returns - The resulting :tl:`Updates` object. - - Example - .. code-block:: python - - from datetime import timedelta - - # Banning `user` from `chat` for 1 minute - await client.edit_permissions(chat, user, timedelta(minutes=1), - view_messages=False) - - # Banning `user` from `chat` forever - await client.edit_permissions(chat, user, view_messages=False) - - # Kicking someone (ban + un-ban) - await client.edit_permissions(chat, user, view_messages=False) - await client.edit_permissions(chat, user) - """ - entity = await self.get_input_entity(entity) - ty = helpers._entity_type(entity) - if ty != helpers._EntityType.CHANNEL: - raise ValueError('You must pass either a channel or a supergroup') - - rights = types.ChatBannedRights( - until_date=until_date, - view_messages=not view_messages, - send_messages=not send_messages, - send_media=not send_media, - send_stickers=not send_stickers, - send_gifs=not send_gifs, - send_games=not send_games, - send_inline=not send_inline, - embed_links=not embed_link_previews, - send_polls=not send_polls, - change_info=not change_info, - invite_users=not invite_users, - pin_messages=not pin_messages - ) - - if user is None: - return await self(functions.messages.EditChatDefaultBannedRightsRequest( - peer=entity, - banned_rights=rights - )) - - user = await self.get_input_entity(user) - ty = helpers._entity_type(user) - if ty != helpers._EntityType.USER: - raise ValueError('You must pass a user entity') - - if isinstance(user, types.InputPeerSelf): - raise ValueError('You cannot restrict yourself') - - return await self(functions.channels.EditBannedRequest( - channel=entity, - participant=user, + if user is None: + return await self(functions.messages.EditChatDefaultBannedRightsRequest( + peer=entity, banned_rights=rights )) - async def kick_participant( - self: 'TelegramClient', - entity: 'hints.EntityLike', - user: 'typing.Optional[hints.EntityLike]' - ): - """ - Kicks a user from a chat. + user = await self.get_input_entity(user) + ty = helpers._entity_type(user) + if ty != helpers._EntityType.USER: + raise ValueError('You must pass a user entity') - Kicking yourself (``'me'``) will result in leaving the chat. + if isinstance(user, types.InputPeerSelf): + raise ValueError('You cannot restrict yourself') - .. note:: + return await self(functions.channels.EditBannedRequest( + channel=entity, + participant=user, + banned_rights=rights + )) - Attempting to kick someone who was banned will remove their - restrictions (and thus unbanning them), since kicking is just - ban + unban. +async def kick_participant( + self: 'TelegramClient', + entity: 'hints.EntityLike', + user: 'typing.Optional[hints.EntityLike]' +): + entity = await self.get_input_entity(entity) + user = await self.get_input_entity(user) + if helpers._entity_type(user) != helpers._EntityType.USER: + raise ValueError('You must pass a user entity') - Arguments - entity (`entity`): - The channel or chat where the user should be kicked from. - - user (`entity`, optional): - The user to kick. - - Returns - Returns the service `Message ` - produced about a user being kicked, if any. - - Example - .. code-block:: python - - # Kick some user from some chat, and deleting the service message - msg = await client.kick_participant(chat, user) - await msg.delete() - - # Leaving chat - await client.kick_participant(chat, 'me') - """ - entity = await self.get_input_entity(entity) - user = await self.get_input_entity(user) - if helpers._entity_type(user) != helpers._EntityType.USER: - raise ValueError('You must pass a user entity') - - ty = helpers._entity_type(entity) - if ty == helpers._EntityType.CHAT: - resp = await self(functions.messages.DeleteChatUserRequest(entity.chat_id, user)) - elif ty == helpers._EntityType.CHANNEL: - if isinstance(user, types.InputPeerSelf): - # Despite no longer being in the channel, the account still - # seems to get the service message. - resp = await self(functions.channels.LeaveChannelRequest(entity)) - else: - resp = await self(functions.channels.EditBannedRequest( - channel=entity, - participant=user, - banned_rights=types.ChatBannedRights( - until_date=None, view_messages=True) - )) - await asyncio.sleep(0.5) - await self(functions.channels.EditBannedRequest( - channel=entity, - participant=user, - banned_rights=types.ChatBannedRights(until_date=None) - )) + ty = helpers._entity_type(entity) + if ty == helpers._EntityType.CHAT: + resp = await self(functions.messages.DeleteChatUserRequest(entity.chat_id, user)) + elif ty == helpers._EntityType.CHANNEL: + if isinstance(user, types.InputPeerSelf): + # Despite no longer being in the channel, the account still + # seems to get the service message. + resp = await self(functions.channels.LeaveChannelRequest(entity)) else: - raise ValueError('You must pass either a channel or a chat') - - return self._get_response_message(None, resp, entity) - - async def get_permissions( - self: 'TelegramClient', - entity: 'hints.EntityLike', - user: 'hints.EntityLike' = None - ) -> 'typing.Optional[custom.ParticipantPermissions]': - """ - Fetches the permissions of a user in a specific chat or channel or - get Default Restricted Rights of Chat or Channel. - - .. note:: - - This request has to fetch the entire chat for small group chats, - which can get somewhat expensive, so use of a cache is advised. - - Arguments - entity (`entity`): - The channel or chat the user is participant of. - - user (`entity`, optional): - Target user. - - Returns - A `ParticipantPermissions ` - instance. Refer to its documentation to see what properties are - available. - - Example - .. code-block:: python - - permissions = await client.get_permissions(chat, user) - if permissions.is_admin: - # do something - - # Get Banned Permissions of Chat - await client.get_permissions(chat) - """ - entity = await self.get_entity(entity) - - if not user: - if isinstance(entity, types.Channel): - FullChat = await self(functions.channels.GetFullChannelRequest(entity)) - elif isinstance(entity, types.Chat): - FullChat = await self(functions.messages.GetFullChatRequest(entity)) - else: - return - return FullChat.chats[0].default_banned_rights - - entity = await self.get_input_entity(entity) - user = await self.get_input_entity(user) - if helpers._entity_type(user) != helpers._EntityType.USER: - raise ValueError('You must pass a user entity') - if helpers._entity_type(entity) == helpers._EntityType.CHANNEL: - participant = await self(functions.channels.GetParticipantRequest( - entity, - user + resp = await self(functions.channels.EditBannedRequest( + channel=entity, + participant=user, + banned_rights=types.ChatBannedRights( + until_date=None, view_messages=True) )) - return custom.ParticipantPermissions(participant.participant, False) - elif helpers._entity_type(entity) == helpers._EntityType.CHAT: - chat = await self(functions.messages.GetFullChatRequest( - entity + await asyncio.sleep(0.5) + await self(functions.channels.EditBannedRequest( + channel=entity, + participant=user, + banned_rights=types.ChatBannedRights(until_date=None) )) - if isinstance(user, types.InputPeerSelf): - user = await self.get_me(input_peer=True) - for participant in chat.full_chat.participants.participants: - if participant.user_id == user.user_id: - return custom.ParticipantPermissions(participant, True) - raise errors.UserNotParticipantError(None) - + else: raise ValueError('You must pass either a channel or a chat') - async def get_stats( - self: 'TelegramClient', - entity: 'hints.EntityLike', - message: 'typing.Union[int, types.Message]' = None, - ): - """ - Retrieves statistics from the given megagroup or broadcast channel. + return self._get_response_message(None, resp, entity) - Note that some restrictions apply before being able to fetch statistics, - in particular the channel must have enough members (for megagroups, this - requires `at least 500 members`_). +async def get_permissions( + self: 'TelegramClient', + entity: 'hints.EntityLike', + user: 'hints.EntityLike' = None +) -> 'typing.Optional[custom.ParticipantPermissions]': + entity = await self.get_entity(entity) - Arguments - entity (`entity`): - The channel from which to get statistics. - - message (`int` | ``Message``, optional): - The message ID from which to get statistics, if your goal is - to obtain the statistics of a single message. - - Raises - If the given entity is not a channel (broadcast or megagroup), - a `TypeError` is raised. - - If there are not enough members (poorly named) errors such as - ``telethon.errors.ChatAdminRequiredError`` will appear. - - Returns - If both ``entity`` and ``message`` were provided, returns - :tl:`MessageStats`. Otherwise, either :tl:`BroadcastStats` or - :tl:`MegagroupStats`, depending on whether the input belonged to a - broadcast channel or megagroup. - - Example - .. code-block:: python - - # Some megagroup or channel username or ID to fetch - channel = -100123 - stats = await client.get_stats(channel) - print('Stats from', stats.period.min_date, 'to', stats.period.max_date, ':') - print(stats.stringify()) - - .. _`at least 500 members`: https://telegram.org/blog/profile-videos-people-nearby-and-more - """ - entity = await self.get_input_entity(entity) - if helpers._entity_type(entity) != helpers._EntityType.CHANNEL: - raise TypeError('You must pass a channel entity') - - message = utils.get_message_id(message) - if message is not None: - try: - req = functions.stats.GetMessageStatsRequest(entity, message) - return await self(req) - except errors.StatsMigrateError as e: - dc = e.dc + if not user: + if isinstance(entity, types.Channel): + FullChat = await self(functions.channels.GetFullChannelRequest(entity)) + elif isinstance(entity, types.Chat): + FullChat = await self(functions.messages.GetFullChatRequest(entity)) else: - # Don't bother fetching the Channel entity (costs a request), instead - # try to guess and if it fails we know it's the other one (best case - # no extra request, worst just one). + return + return FullChat.chats[0].default_banned_rights + + entity = await self.get_input_entity(entity) + user = await self.get_input_entity(user) + if helpers._entity_type(user) != helpers._EntityType.USER: + raise ValueError('You must pass a user entity') + if helpers._entity_type(entity) == helpers._EntityType.CHANNEL: + participant = await self(functions.channels.GetParticipantRequest( + entity, + user + )) + return custom.ParticipantPermissions(participant.participant, False) + elif helpers._entity_type(entity) == helpers._EntityType.CHAT: + chat = await self(functions.messages.GetFullChatRequest( + entity + )) + if isinstance(user, types.InputPeerSelf): + user = await self.get_me(input_peer=True) + for participant in chat.full_chat.participants.participants: + if participant.user_id == user.user_id: + return custom.ParticipantPermissions(participant, True) + raise errors.UserNotParticipantError(None) + + raise ValueError('You must pass either a channel or a chat') + +async def get_stats( + self: 'TelegramClient', + entity: 'hints.EntityLike', + message: 'typing.Union[int, types.Message]' = None, +): + entity = await self.get_input_entity(entity) + if helpers._entity_type(entity) != helpers._EntityType.CHANNEL: + raise TypeError('You must pass a channel entity') + + message = utils.get_message_id(message) + if message is not None: + try: + req = functions.stats.GetMessageStatsRequest(entity, message) + return await self(req) + except errors.StatsMigrateError as e: + dc = e.dc + else: + # Don't bother fetching the Channel entity (costs a request), instead + # try to guess and if it fails we know it's the other one (best case + # no extra request, worst just one). + try: + req = functions.stats.GetBroadcastStatsRequest(entity) + return await self(req) + except errors.StatsMigrateError as e: + dc = e.dc + except errors.BroadcastRequiredError: + req = functions.stats.GetMegagroupStatsRequest(entity) try: - req = functions.stats.GetBroadcastStatsRequest(entity) return await self(req) except errors.StatsMigrateError as e: dc = e.dc - except errors.BroadcastRequiredError: - req = functions.stats.GetMegagroupStatsRequest(entity) - try: - return await self(req) - except errors.StatsMigrateError as e: - dc = e.dc - sender = await self._borrow_exported_sender(dc) - try: - # req will be resolved to use the right types inside by now - return await sender.send(req) - finally: - await self._return_exported_sender(sender) - - # endregion + sender = await self._borrow_exported_sender(dc) + try: + # req will be resolved to use the right types inside by now + return await sender.send(req) + finally: + await self._return_exported_sender(sender) diff --git a/telethon/client/dialogs.py b/telethon/client/dialogs.py index 8c0860fc..67c47458 100644 --- a/telethon/client/dialogs.py +++ b/telethon/client/dialogs.py @@ -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 `. - - 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 ` 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 `. - - 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() `. - - 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 ` - 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 - ` - and a subsequent call to `conv.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 `. - - Example - .. code-block:: python - - # denotes outgoing messages you sent - # denotes incoming response messages - with bot.conversation(chat) as conv: - # Hi! - conv.send_message('Hi!') - - # Hello! - hello = conv.get_response() - - # Please tell me your name - conv.send_message('Please tell me your name') - - # ? - name = conv.get_response().raw_text - - while not any(x.isalpha() for x in name): - # Your name didn't have any letters! Try again - conv.send_message("Your name didn't have any letters! Try again") - - # Human - name = conv.get_response().raw_text - - # 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 + ) diff --git a/telethon/client/downloads.py b/telethon/client/downloads.py index 62ba6332..2150dc92 100644 --- a/telethon/client/downloads.py +++ b/telethon/client/downloads.py @@ -192,852 +192,838 @@ class _GenericDownloadIter(_DirectDownloadIter): self.request.offset -= self._stride -class DownloadMethods: +async def download_profile_photo( + self: 'TelegramClient', + entity: 'hints.EntityLike', + file: 'hints.FileLike' = None, + *, + download_big: bool = True) -> typing.Optional[str]: + """ + Downloads the profile photo from the given user, chat or channel. - # region Public methods + Arguments + entity (`entity`): + From who the photo will be downloaded. - async def download_profile_photo( - self: 'TelegramClient', - entity: 'hints.EntityLike', - file: 'hints.FileLike' = None, - *, - download_big: bool = True) -> typing.Optional[str]: - """ - Downloads the profile photo from the given user, chat or channel. + .. note:: - Arguments - entity (`entity`): - From who the photo will be downloaded. + This method expects the full entity (which has the data + to download the photo), not an input variant. - .. note:: + It's possible that sometimes you can't fetch the entity + from its input (since you can get errors like + ``ChannelPrivateError``) but you already have it through + another call, like getting a forwarded message from it. - This method expects the full entity (which has the data - to download the photo), not an input variant. + file (`str` | `file`, optional): + The output file path, directory, or stream-like object. + If the path exists and is a file, it will be overwritten. + If file is the type `bytes`, it will be downloaded in-memory + as a bytestring (e.g. ``file=bytes``). - It's possible that sometimes you can't fetch the entity - from its input (since you can get errors like - ``ChannelPrivateError``) but you already have it through - another call, like getting a forwarded message from it. + download_big (`bool`, optional): + Whether to use the big version of the available photos. - file (`str` | `file`, optional): - The output file path, directory, or stream-like object. - If the path exists and is a file, it will be overwritten. - If file is the type `bytes`, it will be downloaded in-memory - as a bytestring (e.g. ``file=bytes``). + Returns + `None` if no photo was provided, or if it was Empty. On success + the file path is returned since it may differ from the one given. - download_big (`bool`, optional): - Whether to use the big version of the available photos. + Example + .. code-block:: python - Returns - `None` if no photo was provided, or if it was Empty. On success - the file path is returned since it may differ from the one given. + # Download your own profile photo + path = await client.download_profile_photo('me') + print(path) + """ + # hex(crc32(x.encode('ascii'))) for x in + # ('User', 'Chat', 'UserFull', 'ChatFull') + ENTITIES = (0x2da17977, 0xc5af5d94, 0x1f4661b9, 0xd49a2697) + # ('InputPeer', 'InputUser', 'InputChannel') + INPUTS = (0xc91c90b6, 0xe669bf46, 0x40f202fd) + if not isinstance(entity, TLObject) or entity.SUBCLASS_OF_ID in INPUTS: + entity = await self.get_entity(entity) - Example - .. code-block:: python + thumb = -1 if download_big else 0 - # Download your own profile photo - path = await client.download_profile_photo('me') - print(path) - """ - # hex(crc32(x.encode('ascii'))) for x in - # ('User', 'Chat', 'UserFull', 'ChatFull') - ENTITIES = (0x2da17977, 0xc5af5d94, 0x1f4661b9, 0xd49a2697) - # ('InputPeer', 'InputUser', 'InputChannel') - INPUTS = (0xc91c90b6, 0xe669bf46, 0x40f202fd) - if not isinstance(entity, TLObject) or entity.SUBCLASS_OF_ID in INPUTS: - entity = await self.get_entity(entity) - - thumb = -1 if download_big else 0 - - possible_names = [] - if entity.SUBCLASS_OF_ID not in ENTITIES: - photo = entity - else: - if not hasattr(entity, 'photo'): - # Special case: may be a ChatFull with photo:Photo - # This is different from a normal UserProfilePhoto and Chat - if not hasattr(entity, 'chat_photo'): - return None - - return await self._download_photo( - entity.chat_photo, file, date=None, - thumb=thumb, progress_callback=None - ) - - for attr in ('username', 'first_name', 'title'): - possible_names.append(getattr(entity, attr, None)) - - photo = entity.photo - - if isinstance(photo, (types.UserProfilePhoto, types.ChatPhoto)): - dc_id = photo.dc_id - loc = types.InputPeerPhotoFileLocation( - peer=await self.get_input_entity(entity), - photo_id=photo.photo_id, - big=download_big - ) - else: - # It doesn't make any sense to check if `photo` can be used - # as input location, because then this method would be able - # to "download the profile photo of a message", i.e. its - # media which should be done with `download_media` instead. - return None - - file = self._get_proper_filename( - file, 'profile_photo', '.jpg', - possible_names=possible_names - ) - - try: - result = await self.download_file(loc, file, dc_id=dc_id) - return result if file is bytes else file - except errors.LocationInvalidError: - # See issue #500, Android app fails as of v4.6.0 (1155). - # The fix seems to be using the full channel chat photo. - ie = await self.get_input_entity(entity) - ty = helpers._entity_type(ie) - if ty == helpers._EntityType.CHANNEL: - full = await self(functions.channels.GetFullChannelRequest(ie)) - return await self._download_photo( - full.full_chat.chat_photo, file, - date=None, progress_callback=None, - thumb=thumb - ) - else: - # Until there's a report for chats, no need to. + possible_names = [] + if entity.SUBCLASS_OF_ID not in ENTITIES: + photo = entity + else: + if not hasattr(entity, 'photo'): + # Special case: may be a ChatFull with photo:Photo + # This is different from a normal UserProfilePhoto and Chat + if not hasattr(entity, 'chat_photo'): return None - async def download_media( - self: 'TelegramClient', - message: 'hints.MessageLike', - file: 'hints.FileLike' = None, - *, - thumb: 'typing.Union[int, types.TypePhotoSize]' = None, - progress_callback: 'hints.ProgressCallback' = None) -> typing.Optional[typing.Union[str, bytes]]: - """ - Downloads the given media from a message object. - - Note that if the download is too slow, you should consider installing - ``cryptg`` (through ``pip install cryptg``) so that decrypting the - received data is done in C instead of Python (much faster). - - See also `Message.download_media() `. - - Arguments - message (`Message ` | :tl:`Media`): - The media or message containing the media that will be downloaded. - - file (`str` | `file`, optional): - The output file path, directory, or stream-like object. - If the path exists and is a file, it will be overwritten. - If file is the type `bytes`, it will be downloaded in-memory - as a bytestring (e.g. ``file=bytes``). - - progress_callback (`callable`, optional): - A callback function accepting two parameters: - ``(received bytes, total)``. - - thumb (`int` | :tl:`PhotoSize`, optional): - Which thumbnail size from the document or photo to download, - instead of downloading the document or photo itself. - - If it's specified but the file does not have a thumbnail, - this method will return `None`. - - The parameter should be an integer index between ``0`` and - ``len(sizes)``. ``0`` will download the smallest thumbnail, - and ``len(sizes) - 1`` will download the largest thumbnail. - You can also use negative indices, which work the same as - they do in Python's `list`. - - You can also pass the :tl:`PhotoSize` instance to use. - Alternatively, the thumb size type `str` may be used. - - In short, use ``thumb=0`` if you want the smallest thumbnail - and ``thumb=-1`` if you want the largest thumbnail. - - .. note:: - The largest thumbnail may be a video instead of a photo, - as they are available since layer 116 and are bigger than - any of the photos. - - Returns - `None` if no media was provided, or if it was Empty. On success - the file path is returned since it may differ from the one given. - - Example - .. code-block:: python - - path = await client.download_media(message) - await client.download_media(message, filename) - # or - path = await message.download_media() - await message.download_media(filename) - - # Printing download progress - def callback(current, total): - print('Downloaded', current, 'out of', total, - 'bytes: {:.2%}'.format(current / total)) - - await client.download_media(message, progress_callback=callback) - """ - # Downloading large documents may be slow enough to require a new file reference - # to be obtained mid-download. Store (input chat, message id) so that the message - # can be re-fetched. - msg_data = None - - # TODO This won't work for messageService - if isinstance(message, types.Message): - date = message.date - media = message.media - msg_data = (message.input_chat, message.id) if message.input_chat else None - else: - date = datetime.datetime.now() - media = message - - if isinstance(media, str): - media = utils.resolve_bot_file_id(media) - - if isinstance(media, types.MessageService): - if isinstance(message.action, - types.MessageActionChatEditPhoto): - media = media.photo - - if isinstance(media, types.MessageMediaWebPage): - if isinstance(media.webpage, types.WebPage): - media = media.webpage.document or media.webpage.photo - - if isinstance(media, (types.MessageMediaPhoto, types.Photo)): return await self._download_photo( - media, file, date, thumb, progress_callback - ) - elif isinstance(media, (types.MessageMediaDocument, types.Document)): - return await self._download_document( - media, file, date, thumb, progress_callback, msg_data - ) - elif isinstance(media, types.MessageMediaContact) and thumb is None: - return self._download_contact( - media, file - ) - elif isinstance(media, (types.WebDocument, types.WebDocumentNoProxy)) and thumb is None: - return await self._download_web_document( - media, file, progress_callback + entity.chat_photo, file, date=None, + thumb=thumb, progress_callback=None ) - async def download_file( - self: 'TelegramClient', - input_location: 'hints.FileLike', - file: 'hints.OutFileLike' = None, - *, - part_size_kb: float = None, - file_size: int = None, - progress_callback: 'hints.ProgressCallback' = None, - dc_id: int = None, - key: bytes = None, - iv: bytes = None) -> typing.Optional[bytes]: - """ - Low-level method to download files from their input location. + for attr in ('username', 'first_name', 'title'): + possible_names.append(getattr(entity, attr, None)) - .. note:: + photo = entity.photo - Generally, you should instead use `download_media`. - This method is intended to be a bit more low-level. + if isinstance(photo, (types.UserProfilePhoto, types.ChatPhoto)): + dc_id = photo.dc_id + loc = types.InputPeerPhotoFileLocation( + peer=await self.get_input_entity(entity), + photo_id=photo.photo_id, + big=download_big + ) + else: + # It doesn't make any sense to check if `photo` can be used + # as input location, because then this method would be able + # to "download the profile photo of a message", i.e. its + # media which should be done with `download_media` instead. + return None - Arguments - input_location (:tl:`InputFileLocation`): - The file location from which the file will be downloaded. - See `telethon.utils.get_input_location` source for a complete - list of supported types. + file = self._get_proper_filename( + file, 'profile_photo', '.jpg', + possible_names=possible_names + ) - file (`str` | `file`, optional): - The output file path, directory, or stream-like object. - If the path exists and is a file, it will be overwritten. + try: + result = await self.download_file(loc, file, dc_id=dc_id) + return result if file is bytes else file + except errors.LocationInvalidError: + # See issue #500, Android app fails as of v4.6.0 (1155). + # The fix seems to be using the full channel chat photo. + ie = await self.get_input_entity(entity) + ty = helpers._entity_type(ie) + if ty == helpers._EntityType.CHANNEL: + full = await self(functions.channels.GetFullChannelRequest(ie)) + return await self._download_photo( + full.full_chat.chat_photo, file, + date=None, progress_callback=None, + thumb=thumb + ) + else: + # Until there's a report for chats, no need to. + return None - If the file path is `None` or `bytes`, then the result - will be saved in memory and returned as `bytes`. +async def download_media( + self: 'TelegramClient', + message: 'hints.MessageLike', + file: 'hints.FileLike' = None, + *, + thumb: 'typing.Union[int, types.TypePhotoSize]' = None, + progress_callback: 'hints.ProgressCallback' = None) -> typing.Optional[typing.Union[str, bytes]]: + """ + Downloads the given media from a message object. - part_size_kb (`int`, optional): - Chunk size when downloading files. The larger, the less - requests will be made (up to 512KB maximum). + Note that if the download is too slow, you should consider installing + ``cryptg`` (through ``pip install cryptg``) so that decrypting the + received data is done in C instead of Python (much faster). - file_size (`int`, optional): - The file size that is about to be downloaded, if known. - Only used if ``progress_callback`` is specified. + See also `Message.download_media() `. - progress_callback (`callable`, optional): - A callback function accepting two parameters: - ``(downloaded bytes, total)``. Note that the - ``total`` is the provided ``file_size``. + Arguments + message (`Message ` | :tl:`Media`): + The media or message containing the media that will be downloaded. - dc_id (`int`, optional): - The data center the library should connect to in order - to download the file. You shouldn't worry about this. + file (`str` | `file`, optional): + The output file path, directory, or stream-like object. + If the path exists and is a file, it will be overwritten. + If file is the type `bytes`, it will be downloaded in-memory + as a bytestring (e.g. ``file=bytes``). - key ('bytes', optional): - In case of an encrypted upload (secret chats) a key is supplied + progress_callback (`callable`, optional): + A callback function accepting two parameters: + ``(received bytes, total)``. - iv ('bytes', optional): - In case of an encrypted upload (secret chats) an iv is supplied + thumb (`int` | :tl:`PhotoSize`, optional): + Which thumbnail size from the document or photo to download, + instead of downloading the document or photo itself. + If it's specified but the file does not have a thumbnail, + this method will return `None`. - Example - .. code-block:: python + The parameter should be an integer index between ``0`` and + ``len(sizes)``. ``0`` will download the smallest thumbnail, + and ``len(sizes) - 1`` will download the largest thumbnail. + You can also use negative indices, which work the same as + they do in Python's `list`. - # Download a file and print its header - data = await client.download_file(input_file, bytes) - print(data[:16]) - """ - return await self._download_file( - input_location, - file, - part_size_kb=part_size_kb, - file_size=file_size, - progress_callback=progress_callback, - dc_id=dc_id, - key=key, - iv=iv, + You can also pass the :tl:`PhotoSize` instance to use. + Alternatively, the thumb size type `str` may be used. + + In short, use ``thumb=0`` if you want the smallest thumbnail + and ``thumb=-1`` if you want the largest thumbnail. + + .. note:: + The largest thumbnail may be a video instead of a photo, + as they are available since layer 116 and are bigger than + any of the photos. + + Returns + `None` if no media was provided, or if it was Empty. On success + the file path is returned since it may differ from the one given. + + Example + .. code-block:: python + + path = await client.download_media(message) + await client.download_media(message, filename) + # or + path = await message.download_media() + await message.download_media(filename) + + # Printing download progress + def callback(current, total): + print('Downloaded', current, 'out of', total, + 'bytes: {:.2%}'.format(current / total)) + + await client.download_media(message, progress_callback=callback) + """ + # Downloading large documents may be slow enough to require a new file reference + # to be obtained mid-download. Store (input chat, message id) so that the message + # can be re-fetched. + msg_data = None + + # TODO This won't work for messageService + if isinstance(message, types.Message): + date = message.date + media = message.media + msg_data = (message.input_chat, message.id) if message.input_chat else None + else: + date = datetime.datetime.now() + media = message + + if isinstance(media, str): + media = utils.resolve_bot_file_id(media) + + if isinstance(media, types.MessageService): + if isinstance(message.action, + types.MessageActionChatEditPhoto): + media = media.photo + + if isinstance(media, types.MessageMediaWebPage): + if isinstance(media.webpage, types.WebPage): + media = media.webpage.document or media.webpage.photo + + if isinstance(media, (types.MessageMediaPhoto, types.Photo)): + return await self._download_photo( + media, file, date, thumb, progress_callback + ) + elif isinstance(media, (types.MessageMediaDocument, types.Document)): + return await self._download_document( + media, file, date, thumb, progress_callback, msg_data + ) + elif isinstance(media, types.MessageMediaContact) and thumb is None: + return self._download_contact( + media, file + ) + elif isinstance(media, (types.WebDocument, types.WebDocumentNoProxy)) and thumb is None: + return await self._download_web_document( + media, file, progress_callback ) - async def _download_file( - self: 'TelegramClient', - input_location: 'hints.FileLike', - file: 'hints.OutFileLike' = None, - *, - part_size_kb: float = None, - file_size: int = None, - progress_callback: 'hints.ProgressCallback' = None, - dc_id: int = None, - key: bytes = None, - iv: bytes = None, - msg_data: tuple = None) -> typing.Optional[bytes]: - if not part_size_kb: - if not file_size: - part_size_kb = 64 # Reasonable default - else: - part_size_kb = utils.get_appropriated_part_size(file_size) +async def download_file( + self: 'TelegramClient', + input_location: 'hints.FileLike', + file: 'hints.OutFileLike' = None, + *, + part_size_kb: float = None, + file_size: int = None, + progress_callback: 'hints.ProgressCallback' = None, + dc_id: int = None, + key: bytes = None, + iv: bytes = None) -> typing.Optional[bytes]: + """ + Low-level method to download files from their input location. - part_size = int(part_size_kb * 1024) - if part_size % MIN_CHUNK_SIZE != 0: - raise ValueError( - 'The part size must be evenly divisible by 4096.') + .. note:: - if isinstance(file, pathlib.Path): - file = str(file.absolute()) + Generally, you should instead use `download_media`. + This method is intended to be a bit more low-level. - in_memory = file is None or file is bytes - if in_memory: - f = io.BytesIO() - elif isinstance(file, str): - # Ensure that we'll be able to download the media - helpers.ensure_parent_dir_exists(file) - f = open(file, 'wb') + Arguments + input_location (:tl:`InputFileLocation`): + The file location from which the file will be downloaded. + See `telethon.utils.get_input_location` source for a complete + list of supported types. + + file (`str` | `file`, optional): + The output file path, directory, or stream-like object. + If the path exists and is a file, it will be overwritten. + + If the file path is `None` or `bytes`, then the result + will be saved in memory and returned as `bytes`. + + part_size_kb (`int`, optional): + Chunk size when downloading files. The larger, the less + requests will be made (up to 512KB maximum). + + file_size (`int`, optional): + The file size that is about to be downloaded, if known. + Only used if ``progress_callback`` is specified. + + progress_callback (`callable`, optional): + A callback function accepting two parameters: + ``(downloaded bytes, total)``. Note that the + ``total`` is the provided ``file_size``. + + dc_id (`int`, optional): + The data center the library should connect to in order + to download the file. You shouldn't worry about this. + + key ('bytes', optional): + In case of an encrypted upload (secret chats) a key is supplied + + iv ('bytes', optional): + In case of an encrypted upload (secret chats) an iv is supplied + + + Example + .. code-block:: python + + # Download a file and print its header + data = await client.download_file(input_file, bytes) + print(data[:16]) + """ + return await self._download_file( + input_location, + file, + part_size_kb=part_size_kb, + file_size=file_size, + progress_callback=progress_callback, + dc_id=dc_id, + key=key, + iv=iv, + ) + +async def _download_file( + self: 'TelegramClient', + input_location: 'hints.FileLike', + file: 'hints.OutFileLike' = None, + *, + part_size_kb: float = None, + file_size: int = None, + progress_callback: 'hints.ProgressCallback' = None, + dc_id: int = None, + key: bytes = None, + iv: bytes = None, + msg_data: tuple = None) -> typing.Optional[bytes]: + if not part_size_kb: + if not file_size: + part_size_kb = 64 # Reasonable default else: - f = file + part_size_kb = utils.get_appropriated_part_size(file_size) - try: - async for chunk in self._iter_download( - input_location, request_size=part_size, dc_id=dc_id, msg_data=msg_data): - if iv and key: - chunk = AES.decrypt_ige(chunk, key, iv) - r = f.write(chunk) + part_size = int(part_size_kb * 1024) + if part_size % MIN_CHUNK_SIZE != 0: + raise ValueError( + 'The part size must be evenly divisible by 4096.') + + if isinstance(file, pathlib.Path): + file = str(file.absolute()) + + in_memory = file is None or file is bytes + if in_memory: + f = io.BytesIO() + elif isinstance(file, str): + # Ensure that we'll be able to download the media + helpers.ensure_parent_dir_exists(file) + f = open(file, 'wb') + else: + f = file + + try: + async for chunk in self._iter_download( + input_location, request_size=part_size, dc_id=dc_id, msg_data=msg_data): + if iv and key: + chunk = AES.decrypt_ige(chunk, key, iv) + r = f.write(chunk) + if inspect.isawaitable(r): + await r + + if progress_callback: + r = progress_callback(f.tell(), file_size) if inspect.isawaitable(r): await r - if progress_callback: - r = progress_callback(f.tell(), file_size) - if inspect.isawaitable(r): - await r + # Not all IO objects have flush (see #1227) + if callable(getattr(f, 'flush', None)): + f.flush() - # Not all IO objects have flush (see #1227) - if callable(getattr(f, 'flush', None)): - f.flush() + if in_memory: + return f.getvalue() + finally: + if isinstance(file, str) or in_memory: + f.close() - if in_memory: - return f.getvalue() - finally: - if isinstance(file, str) or in_memory: - f.close() +def iter_download( + self: 'TelegramClient', + file: 'hints.FileLike', + *, + offset: int = 0, + stride: int = None, + limit: int = None, + chunk_size: int = None, + request_size: int = MAX_CHUNK_SIZE, + file_size: int = None, + dc_id: int = None +): + """ + Iterates over a file download, yielding chunks of the file. - def iter_download( - self: 'TelegramClient', - file: 'hints.FileLike', - *, - offset: int = 0, - stride: int = None, - limit: int = None, - chunk_size: int = None, - request_size: int = MAX_CHUNK_SIZE, - file_size: int = None, - dc_id: int = None - ): - """ - Iterates over a file download, yielding chunks of the file. + This method can be used to stream files in a more convenient + way, since it offers more control (pausing, resuming, etc.) - This method can be used to stream files in a more convenient - way, since it offers more control (pausing, resuming, etc.) + .. note:: - .. note:: + Using a value for `offset` or `stride` which is not a multiple + of the minimum allowed `request_size`, or if `chunk_size` is + different from `request_size`, the library will need to do a + bit more work to fetch the data in the way you intend it to. - Using a value for `offset` or `stride` which is not a multiple - of the minimum allowed `request_size`, or if `chunk_size` is - different from `request_size`, the library will need to do a - bit more work to fetch the data in the way you intend it to. + You normally shouldn't worry about this. - You normally shouldn't worry about this. + Arguments + file (`hints.FileLike`): + The file of which contents you want to iterate over. - Arguments - file (`hints.FileLike`): - The file of which contents you want to iterate over. + offset (`int`, optional): + The offset in bytes into the file from where the + download should start. For example, if a file is + 1024KB long and you just want the last 512KB, you + would use ``offset=512 * 1024``. - offset (`int`, optional): - The offset in bytes into the file from where the - download should start. For example, if a file is - 1024KB long and you just want the last 512KB, you - would use ``offset=512 * 1024``. + stride (`int`, optional): + The stride of each chunk (how much the offset should + advance between reading each chunk). This parameter + should only be used for more advanced use cases. - stride (`int`, optional): - The stride of each chunk (how much the offset should - advance between reading each chunk). This parameter - should only be used for more advanced use cases. + It must be bigger than or equal to the `chunk_size`. - It must be bigger than or equal to the `chunk_size`. + limit (`int`, optional): + The limit for how many *chunks* will be yielded at most. - limit (`int`, optional): - The limit for how many *chunks* will be yielded at most. + chunk_size (`int`, optional): + The maximum size of the chunks that will be yielded. + Note that the last chunk may be less than this value. + By default, it equals to `request_size`. - chunk_size (`int`, optional): - The maximum size of the chunks that will be yielded. - Note that the last chunk may be less than this value. - By default, it equals to `request_size`. + request_size (`int`, optional): + How many bytes will be requested to Telegram when more + data is required. By default, as many bytes as possible + are requested. If you would like to request data in + smaller sizes, adjust this parameter. - request_size (`int`, optional): - How many bytes will be requested to Telegram when more - data is required. By default, as many bytes as possible - are requested. If you would like to request data in - smaller sizes, adjust this parameter. + Note that values outside the valid range will be clamped, + and the final value will also be a multiple of the minimum + allowed size. - Note that values outside the valid range will be clamped, - and the final value will also be a multiple of the minimum - allowed size. + file_size (`int`, optional): + If the file size is known beforehand, you should set + this parameter to said value. Depending on the type of + the input file passed, this may be set automatically. - file_size (`int`, optional): - If the file size is known beforehand, you should set - this parameter to said value. Depending on the type of - the input file passed, this may be set automatically. + dc_id (`int`, optional): + The data center the library should connect to in order + to download the file. You shouldn't worry about this. - dc_id (`int`, optional): - The data center the library should connect to in order - to download the file. You shouldn't worry about this. + Yields - Yields + `bytes` objects representing the chunks of the file if the + right conditions are met, or `memoryview` objects instead. - `bytes` objects representing the chunks of the file if the - right conditions are met, or `memoryview` objects instead. + Example + .. code-block:: python - Example - .. code-block:: python + # Streaming `media` to an output file + # After the iteration ends, the sender is cleaned up + with open('photo.jpg', 'wb') as fd: + async for chunk in client.iter_download(media): + fd.write(chunk) - # Streaming `media` to an output file - # After the iteration ends, the sender is cleaned up - with open('photo.jpg', 'wb') as fd: - async for chunk in client.iter_download(media): - fd.write(chunk) + # Fetching only the header of a file (32 bytes) + # You should manually close the iterator in this case. + # + # "stream" is a common name for asynchronous generators, + # and iter_download will yield `bytes` (chunks of the file). + stream = client.iter_download(media, request_size=32) + header = await stream.__anext__() # "manual" version of `async for` + await stream.close() + assert len(header) == 32 + """ + return self._iter_download( + file, + offset=offset, + stride=stride, + limit=limit, + chunk_size=chunk_size, + request_size=request_size, + file_size=file_size, + dc_id=dc_id, + ) - # Fetching only the header of a file (32 bytes) - # You should manually close the iterator in this case. - # - # "stream" is a common name for asynchronous generators, - # and iter_download will yield `bytes` (chunks of the file). - stream = client.iter_download(media, request_size=32) - header = await stream.__anext__() # "manual" version of `async for` - await stream.close() - assert len(header) == 32 - """ - return self._iter_download( - file, - offset=offset, - stride=stride, - limit=limit, - chunk_size=chunk_size, - request_size=request_size, - file_size=file_size, - dc_id=dc_id, +def _iter_download( + self: 'TelegramClient', + file: 'hints.FileLike', + *, + offset: int = 0, + stride: int = None, + limit: int = None, + chunk_size: int = None, + request_size: int = MAX_CHUNK_SIZE, + file_size: int = None, + dc_id: int = None, + msg_data: tuple = None +): + info = utils._get_file_info(file) + if info.dc_id is not None: + dc_id = info.dc_id + + if file_size is None: + file_size = info.size + + file = info.location + + if chunk_size is None: + chunk_size = request_size + + if limit is None and file_size is not None: + limit = (file_size + chunk_size - 1) // chunk_size + + if stride is None: + stride = chunk_size + elif stride < chunk_size: + raise ValueError('stride must be >= chunk_size') + + request_size -= request_size % MIN_CHUNK_SIZE + if request_size < MIN_CHUNK_SIZE: + request_size = MIN_CHUNK_SIZE + elif request_size > MAX_CHUNK_SIZE: + request_size = MAX_CHUNK_SIZE + + if chunk_size == request_size \ + and offset % MIN_CHUNK_SIZE == 0 \ + and stride % MIN_CHUNK_SIZE == 0 \ + and (limit is None or offset % limit == 0): + cls = _DirectDownloadIter + self._log[__name__].info('Starting direct file download in chunks of ' + '%d at %d, stride %d', request_size, offset, stride) + else: + cls = _GenericDownloadIter + self._log[__name__].info('Starting indirect file download in chunks of ' + '%d at %d, stride %d', request_size, offset, stride) + + return cls( + self, + limit, + file=file, + dc_id=dc_id, + offset=offset, + stride=stride, + chunk_size=chunk_size, + request_size=request_size, + file_size=file_size, + msg_data=msg_data, + ) + + +def _get_thumb(thumbs, thumb): + # Seems Telegram has changed the order and put `PhotoStrippedSize` + # last while this is the smallest (layer 116). Ensure we have the + # sizes sorted correctly with a custom function. + def sort_thumbs(thumb): + if isinstance(thumb, types.PhotoStrippedSize): + return 1, len(thumb.bytes) + if isinstance(thumb, types.PhotoCachedSize): + return 1, len(thumb.bytes) + if isinstance(thumb, types.PhotoSize): + return 1, thumb.size + if isinstance(thumb, types.PhotoSizeProgressive): + return 1, max(thumb.sizes) + if isinstance(thumb, types.VideoSize): + return 2, thumb.size + + # Empty size or invalid should go last + return 0, 0 + + thumbs = list(sorted(thumbs, key=sort_thumbs)) + + for i in reversed(range(len(thumbs))): + # :tl:`PhotoPathSize` is used for animated stickers preview, and the thumb is actually + # a SVG path of the outline. Users expect thumbnails to be JPEG files, so pretend this + # thumb size doesn't actually exist (#1655). + if isinstance(thumbs[i], types.PhotoPathSize): + thumbs.pop(i) + + if thumb is None: + return thumbs[-1] + elif isinstance(thumb, int): + return thumbs[thumb] + elif isinstance(thumb, str): + return next((t for t in thumbs if t.type == thumb), None) + elif isinstance(thumb, (types.PhotoSize, types.PhotoCachedSize, + types.PhotoStrippedSize, types.VideoSize)): + return thumb + else: + return None + +def _download_cached_photo_size(self: 'TelegramClient', size, file): + # No need to download anything, simply write the bytes + if isinstance(size, types.PhotoStrippedSize): + data = utils.stripped_photo_to_jpg(size.bytes) + else: + data = size.bytes + + if file is bytes: + return data + elif isinstance(file, str): + helpers.ensure_parent_dir_exists(file) + f = open(file, 'wb') + else: + f = file + + try: + f.write(data) + finally: + if isinstance(file, str): + f.close() + return file + +async def _download_photo(self: 'TelegramClient', photo, file, date, thumb, progress_callback): + """Specialized version of .download_media() for photos""" + # Determine the photo and its largest size + if isinstance(photo, types.MessageMediaPhoto): + photo = photo.photo + if not isinstance(photo, types.Photo): + return + + # Include video sizes here (but they may be None so provide an empty list) + size = self._get_thumb(photo.sizes + (photo.video_sizes or []), thumb) + if not size or isinstance(size, types.PhotoSizeEmpty): + return + + if isinstance(size, types.VideoSize): + file = self._get_proper_filename(file, 'video', '.mp4', date=date) + else: + file = self._get_proper_filename(file, 'photo', '.jpg', date=date) + + if isinstance(size, (types.PhotoCachedSize, types.PhotoStrippedSize)): + return self._download_cached_photo_size(size, file) + + if isinstance(size, types.PhotoSizeProgressive): + file_size = max(size.sizes) + else: + file_size = size.size + + result = await self.download_file( + types.InputPhotoFileLocation( + id=photo.id, + access_hash=photo.access_hash, + file_reference=photo.file_reference, + thumb_size=size.type + ), + file, + file_size=file_size, + progress_callback=progress_callback + ) + return result if file is bytes else file + +def _get_kind_and_names(attributes): + """Gets kind and possible names for :tl:`DocumentAttribute`.""" + kind = 'document' + possible_names = [] + for attr in attributes: + if isinstance(attr, types.DocumentAttributeFilename): + possible_names.insert(0, attr.file_name) + + elif isinstance(attr, types.DocumentAttributeAudio): + kind = 'audio' + if attr.performer and attr.title: + possible_names.append('{} - {}'.format( + attr.performer, attr.title + )) + elif attr.performer: + possible_names.append(attr.performer) + elif attr.title: + possible_names.append(attr.title) + elif attr.voice: + kind = 'voice' + + return kind, possible_names + +async def _download_document( + self, document, file, date, thumb, progress_callback, msg_data): + """Specialized version of .download_media() for documents.""" + if isinstance(document, types.MessageMediaDocument): + document = document.document + if not isinstance(document, types.Document): + return + + if thumb is None: + kind, possible_names = self._get_kind_and_names(document.attributes) + file = self._get_proper_filename( + file, kind, utils.get_extension(document), + date=date, possible_names=possible_names ) - - def _iter_download( - self: 'TelegramClient', - file: 'hints.FileLike', - *, - offset: int = 0, - stride: int = None, - limit: int = None, - chunk_size: int = None, - request_size: int = MAX_CHUNK_SIZE, - file_size: int = None, - dc_id: int = None, - msg_data: tuple = None - ): - info = utils._get_file_info(file) - if info.dc_id is not None: - dc_id = info.dc_id - - if file_size is None: - file_size = info.size - - file = info.location - - if chunk_size is None: - chunk_size = request_size - - if limit is None and file_size is not None: - limit = (file_size + chunk_size - 1) // chunk_size - - if stride is None: - stride = chunk_size - elif stride < chunk_size: - raise ValueError('stride must be >= chunk_size') - - request_size -= request_size % MIN_CHUNK_SIZE - if request_size < MIN_CHUNK_SIZE: - request_size = MIN_CHUNK_SIZE - elif request_size > MAX_CHUNK_SIZE: - request_size = MAX_CHUNK_SIZE - - if chunk_size == request_size \ - and offset % MIN_CHUNK_SIZE == 0 \ - and stride % MIN_CHUNK_SIZE == 0 \ - and (limit is None or offset % limit == 0): - cls = _DirectDownloadIter - self._log[__name__].info('Starting direct file download in chunks of ' - '%d at %d, stride %d', request_size, offset, stride) - else: - cls = _GenericDownloadIter - self._log[__name__].info('Starting indirect file download in chunks of ' - '%d at %d, stride %d', request_size, offset, stride) - - return cls( - self, - limit, - file=file, - dc_id=dc_id, - offset=offset, - stride=stride, - chunk_size=chunk_size, - request_size=request_size, - file_size=file_size, - msg_data=msg_data, - ) - - # endregion - - # region Private methods - - @staticmethod - def _get_thumb(thumbs, thumb): - # Seems Telegram has changed the order and put `PhotoStrippedSize` - # last while this is the smallest (layer 116). Ensure we have the - # sizes sorted correctly with a custom function. - def sort_thumbs(thumb): - if isinstance(thumb, types.PhotoStrippedSize): - return 1, len(thumb.bytes) - if isinstance(thumb, types.PhotoCachedSize): - return 1, len(thumb.bytes) - if isinstance(thumb, types.PhotoSize): - return 1, thumb.size - if isinstance(thumb, types.PhotoSizeProgressive): - return 1, max(thumb.sizes) - if isinstance(thumb, types.VideoSize): - return 2, thumb.size - - # Empty size or invalid should go last - return 0, 0 - - thumbs = list(sorted(thumbs, key=sort_thumbs)) - - for i in reversed(range(len(thumbs))): - # :tl:`PhotoPathSize` is used for animated stickers preview, and the thumb is actually - # a SVG path of the outline. Users expect thumbnails to be JPEG files, so pretend this - # thumb size doesn't actually exist (#1655). - if isinstance(thumbs[i], types.PhotoPathSize): - thumbs.pop(i) - - if thumb is None: - return thumbs[-1] - elif isinstance(thumb, int): - return thumbs[thumb] - elif isinstance(thumb, str): - return next((t for t in thumbs if t.type == thumb), None) - elif isinstance(thumb, (types.PhotoSize, types.PhotoCachedSize, - types.PhotoStrippedSize, types.VideoSize)): - return thumb - else: - return None - - def _download_cached_photo_size(self: 'TelegramClient', size, file): - # No need to download anything, simply write the bytes - if isinstance(size, types.PhotoStrippedSize): - data = utils.stripped_photo_to_jpg(size.bytes) - else: - data = size.bytes - - if file is bytes: - return data - elif isinstance(file, str): - helpers.ensure_parent_dir_exists(file) - f = open(file, 'wb') - else: - f = file - - try: - f.write(data) - finally: - if isinstance(file, str): - f.close() - return file - - async def _download_photo(self: 'TelegramClient', photo, file, date, thumb, progress_callback): - """Specialized version of .download_media() for photos""" - # Determine the photo and its largest size - if isinstance(photo, types.MessageMediaPhoto): - photo = photo.photo - if not isinstance(photo, types.Photo): - return - - # Include video sizes here (but they may be None so provide an empty list) - size = self._get_thumb(photo.sizes + (photo.video_sizes or []), thumb) - if not size or isinstance(size, types.PhotoSizeEmpty): - return - - if isinstance(size, types.VideoSize): - file = self._get_proper_filename(file, 'video', '.mp4', date=date) - else: - file = self._get_proper_filename(file, 'photo', '.jpg', date=date) - + size = None + else: + file = self._get_proper_filename(file, 'photo', '.jpg', date=date) + size = self._get_thumb(document.thumbs, thumb) if isinstance(size, (types.PhotoCachedSize, types.PhotoStrippedSize)): return self._download_cached_photo_size(size, file) - if isinstance(size, types.PhotoSizeProgressive): - file_size = max(size.sizes) - else: - file_size = size.size + result = await self._download_file( + types.InputDocumentFileLocation( + id=document.id, + access_hash=document.access_hash, + file_reference=document.file_reference, + thumb_size=size.type if size else '' + ), + file, + file_size=size.size if size else document.size, + progress_callback=progress_callback, + msg_data=msg_data, + ) - result = await self.download_file( - types.InputPhotoFileLocation( - id=photo.id, - access_hash=photo.access_hash, - file_reference=photo.file_reference, - thumb_size=size.type - ), - file, - file_size=file_size, - progress_callback=progress_callback + return result if file is bytes else file + +def _download_contact(cls, mm_contact, file): + """ + Specialized version of .download_media() for contacts. + Will make use of the vCard 4.0 format. + """ + first_name = mm_contact.first_name + last_name = mm_contact.last_name + phone_number = mm_contact.phone_number + + # Remove these pesky characters + first_name = first_name.replace(';', '') + last_name = (last_name or '').replace(';', '') + result = ( + 'BEGIN:VCARD\n' + 'VERSION:4.0\n' + 'N:{f};{l};;;\n' + 'FN:{f} {l}\n' + 'TEL;TYPE=cell;VALUE=uri:tel:+{p}\n' + 'END:VCARD\n' + ).format(f=first_name, l=last_name, p=phone_number).encode('utf-8') + + if file is bytes: + return result + elif isinstance(file, str): + file = cls._get_proper_filename( + file, 'contact', '.vcard', + possible_names=[first_name, phone_number, last_name] ) - return result if file is bytes else file + f = open(file, 'wb') + else: + f = file - @staticmethod - def _get_kind_and_names(attributes): - """Gets kind and possible names for :tl:`DocumentAttribute`.""" - kind = 'document' - possible_names = [] - for attr in attributes: - if isinstance(attr, types.DocumentAttributeFilename): - possible_names.insert(0, attr.file_name) + try: + f.write(result) + finally: + # Only close the stream if we opened it + if isinstance(file, str): + f.close() - elif isinstance(attr, types.DocumentAttributeAudio): - kind = 'audio' - if attr.performer and attr.title: - possible_names.append('{} - {}'.format( - attr.performer, attr.title - )) - elif attr.performer: - possible_names.append(attr.performer) - elif attr.title: - possible_names.append(attr.title) - elif attr.voice: - kind = 'voice' + return file - return kind, possible_names - - async def _download_document( - self, document, file, date, thumb, progress_callback, msg_data): - """Specialized version of .download_media() for documents.""" - if isinstance(document, types.MessageMediaDocument): - document = document.document - if not isinstance(document, types.Document): - return - - if thumb is None: - kind, possible_names = self._get_kind_and_names(document.attributes) - file = self._get_proper_filename( - file, kind, utils.get_extension(document), - date=date, possible_names=possible_names - ) - size = None - else: - file = self._get_proper_filename(file, 'photo', '.jpg', date=date) - size = self._get_thumb(document.thumbs, thumb) - if isinstance(size, (types.PhotoCachedSize, types.PhotoStrippedSize)): - return self._download_cached_photo_size(size, file) - - result = await self._download_file( - types.InputDocumentFileLocation( - id=document.id, - access_hash=document.access_hash, - file_reference=document.file_reference, - thumb_size=size.type if size else '' - ), - file, - file_size=size.size if size else document.size, - progress_callback=progress_callback, - msg_data=msg_data, +async def _download_web_document(cls, web, file, progress_callback): + """ + Specialized version of .download_media() for web documents. + """ + if not aiohttp: + raise ValueError( + 'Cannot download web documents without the aiohttp ' + 'dependency install it (pip install aiohttp)' ) - return result if file is bytes else file + # TODO Better way to get opened handles of files and auto-close + in_memory = file is bytes + if in_memory: + f = io.BytesIO() + elif isinstance(file, str): + kind, possible_names = cls._get_kind_and_names(web.attributes) + file = cls._get_proper_filename( + file, kind, utils.get_extension(web), + possible_names=possible_names + ) + f = open(file, 'wb') + else: + f = file - @classmethod - def _download_contact(cls, mm_contact, file): - """ - Specialized version of .download_media() for contacts. - Will make use of the vCard 4.0 format. - """ - first_name = mm_contact.first_name - last_name = mm_contact.last_name - phone_number = mm_contact.phone_number + try: + with aiohttp.ClientSession() as session: + # TODO Use progress_callback; get content length from response + # https://github.com/telegramdesktop/tdesktop/blob/c7e773dd9aeba94e2be48c032edc9a78bb50234e/Telegram/SourceFiles/ui/images.cpp#L1318-L1319 + async with session.get(web.url) as response: + while True: + chunk = await response.content.read(128 * 1024) + if not chunk: + break + f.write(chunk) + finally: + if isinstance(file, str) or file is bytes: + f.close() - # Remove these pesky characters - first_name = first_name.replace(';', '') - last_name = (last_name or '').replace(';', '') - result = ( - 'BEGIN:VCARD\n' - 'VERSION:4.0\n' - 'N:{f};{l};;;\n' - 'FN:{f} {l}\n' - 'TEL;TYPE=cell;VALUE=uri:tel:+{p}\n' - 'END:VCARD\n' - ).format(f=first_name, l=last_name, p=phone_number).encode('utf-8') + return f.getvalue() if in_memory else file - if file is bytes: - return result - elif isinstance(file, str): - file = cls._get_proper_filename( - file, 'contact', '.vcard', - possible_names=[first_name, phone_number, last_name] - ) - f = open(file, 'wb') - else: - f = file +def _get_proper_filename(file, kind, extension, + date=None, possible_names=None): + """Gets a proper filename for 'file', if this is a path. - try: - f.write(result) - finally: - # Only close the stream if we opened it - if isinstance(file, str): - f.close() + 'kind' should be the kind of the output file (photo, document...) + 'extension' should be the extension to be added to the file if + the filename doesn't have any yet + 'date' should be when this file was originally sent, if known + 'possible_names' should be an ordered list of possible names + If no modification is made to the path, any existing file + will be overwritten. + If any modification is made to the path, this method will + ensure that no existing file will be overwritten. + """ + if isinstance(file, pathlib.Path): + file = str(file.absolute()) + + if file is not None and not isinstance(file, str): + # Probably a stream-like object, we cannot set a filename here return file - @classmethod - async def _download_web_document(cls, web, file, progress_callback): - """ - Specialized version of .download_media() for web documents. - """ - if not aiohttp: - raise ValueError( - 'Cannot download web documents without the aiohttp ' - 'dependency install it (pip install aiohttp)' - ) - - # TODO Better way to get opened handles of files and auto-close - in_memory = file is bytes - if in_memory: - f = io.BytesIO() - elif isinstance(file, str): - kind, possible_names = cls._get_kind_and_names(web.attributes) - file = cls._get_proper_filename( - file, kind, utils.get_extension(web), - possible_names=possible_names - ) - f = open(file, 'wb') - else: - f = file + if file is None: + file = '' + elif os.path.isfile(file): + # Make no modifications to valid existing paths + return file + if os.path.isdir(file) or not file: try: - with aiohttp.ClientSession() as session: - # TODO Use progress_callback; get content length from response - # https://github.com/telegramdesktop/tdesktop/blob/c7e773dd9aeba94e2be48c032edc9a78bb50234e/Telegram/SourceFiles/ui/images.cpp#L1318-L1319 - async with session.get(web.url) as response: - while True: - chunk = await response.content.read(128 * 1024) - if not chunk: - break - f.write(chunk) - finally: - if isinstance(file, str) or file is bytes: - f.close() + name = None if possible_names is None else next( + x for x in possible_names if x + ) + except StopIteration: + name = None - return f.getvalue() if in_memory else file + if not name: + if not date: + date = datetime.datetime.now() + name = '{}_{}-{:02}-{:02}_{:02}-{:02}-{:02}'.format( + kind, + date.year, date.month, date.day, + date.hour, date.minute, date.second, + ) + file = os.path.join(file, name) - @staticmethod - def _get_proper_filename(file, kind, extension, - date=None, possible_names=None): - """Gets a proper filename for 'file', if this is a path. + directory, name = os.path.split(file) + name, ext = os.path.splitext(name) + if not ext: + ext = extension - 'kind' should be the kind of the output file (photo, document...) - 'extension' should be the extension to be added to the file if - the filename doesn't have any yet - 'date' should be when this file was originally sent, if known - 'possible_names' should be an ordered list of possible names + result = os.path.join(directory, name + ext) + if not os.path.isfile(result): + return result - If no modification is made to the path, any existing file - will be overwritten. - If any modification is made to the path, this method will - ensure that no existing file will be overwritten. - """ - if isinstance(file, pathlib.Path): - file = str(file.absolute()) - - if file is not None and not isinstance(file, str): - # Probably a stream-like object, we cannot set a filename here - return file - - if file is None: - file = '' - elif os.path.isfile(file): - # Make no modifications to valid existing paths - return file - - if os.path.isdir(file) or not file: - try: - name = None if possible_names is None else next( - x for x in possible_names if x - ) - except StopIteration: - name = None - - if not name: - if not date: - date = datetime.datetime.now() - name = '{}_{}-{:02}-{:02}_{:02}-{:02}-{:02}'.format( - kind, - date.year, date.month, date.day, - date.hour, date.minute, date.second, - ) - file = os.path.join(file, name) - - directory, name = os.path.split(file) - name, ext = os.path.splitext(name) - if not ext: - ext = extension - - result = os.path.join(directory, name + ext) + i = 1 + while True: + result = os.path.join(directory, '{} ({}){}'.format(name, i, ext)) if not os.path.isfile(result): return result - - i = 1 - while True: - result = os.path.join(directory, '{} ({}){}'.format(name, i, ext)) - if not os.path.isfile(result): - return result - i += 1 - - # endregion + i += 1 diff --git a/telethon/client/messageparse.py b/telethon/client/messageparse.py index 322c541e..72b121a9 100644 --- a/telethon/client/messageparse.py +++ b/telethon/client/messageparse.py @@ -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 + ] diff --git a/telethon/client/messages.py b/telethon/client/messages.py index 01011b58..1dde08ec 100644 --- a/telethon/client/messages.py +++ b/telethon/client/messages.py @@ -320,1129 +320,432 @@ class _IDsIter(RequestIter): self.buffer.append(message) -class MessageMethods: +def iter_messages( + self: 'TelegramClient', + entity: 'hints.EntityLike', + limit: float = None, + *, + offset_date: 'hints.DateLike' = None, + offset_id: int = 0, + max_id: int = 0, + min_id: int = 0, + add_offset: int = 0, + search: str = None, + filter: 'typing.Union[types.TypeMessagesFilter, typing.Type[types.TypeMessagesFilter]]' = None, + from_user: 'hints.EntityLike' = None, + wait_time: float = None, + ids: 'typing.Union[int, typing.Sequence[int]]' = None, + reverse: bool = False, + reply_to: int = None, + scheduled: bool = False +) -> 'typing.Union[_MessagesIter, _IDsIter]': + if ids is not None: + if not utils.is_list_like(ids): + ids = [ids] - # region Public methods - - # region Message retrieval - - def iter_messages( - self: 'TelegramClient', - entity: 'hints.EntityLike', - limit: float = None, - *, - offset_date: 'hints.DateLike' = None, - offset_id: int = 0, - max_id: int = 0, - min_id: int = 0, - add_offset: int = 0, - search: str = None, - filter: 'typing.Union[types.TypeMessagesFilter, typing.Type[types.TypeMessagesFilter]]' = None, - from_user: 'hints.EntityLike' = None, - wait_time: float = None, - ids: 'typing.Union[int, typing.Sequence[int]]' = None, - reverse: bool = False, - reply_to: int = None, - scheduled: bool = False - ) -> 'typing.Union[_MessagesIter, _IDsIter]': - """ - Iterator over the messages for the given chat. - - The default order is from newest to oldest, but this - behaviour can be changed with the `reverse` parameter. - - If either `search`, `filter` or `from_user` are provided, - :tl:`messages.Search` will be used instead of :tl:`messages.getHistory`. - - .. note:: - - Telegram's flood wait limit for :tl:`GetHistoryRequest` seems to - be around 30 seconds per 10 requests, therefore a sleep of 1 - second is the default for this limit (or above). - - Arguments - entity (`entity`): - The entity from whom to retrieve the message history. - - It may be `None` to perform a global search, or - to get messages by their ID from no particular chat. - Note that some of the offsets will not work if this - is the case. - - Note that if you want to perform a global search, - you **must** set a non-empty `search` string, a `filter`. - or `from_user`. - - limit (`int` | `None`, optional): - Number of messages to be retrieved. Due to limitations with - the API retrieving more than 3000 messages will take longer - than half a minute (or even more based on previous calls). - - The limit may also be `None`, which would eventually return - the whole history. - - offset_date (`datetime`): - Offset date (messages *previous* to this date will be - retrieved). Exclusive. - - offset_id (`int`): - Offset message ID (only messages *previous* to the given - ID will be retrieved). Exclusive. - - max_id (`int`): - All the messages with a higher (newer) ID or equal to this will - be excluded. - - min_id (`int`): - All the messages with a lower (older) ID or equal to this will - be excluded. - - add_offset (`int`): - 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 entity will be returned. - - wait_time (`int`): - Wait time (in seconds) between different - :tl:`GetHistoryRequest`. Use this parameter to avoid hitting - the ``FloodWaitError`` as needed. If left to `None`, it will - default to 1 second only if the limit is higher than 3000. - - If the ``ids`` parameter is used, this time will default - to 10 seconds only if the amount of IDs is higher than 300. - - ids (`int`, `list`): - A single integer ID (or several IDs) for the message that - should be returned. This parameter takes precedence over - the rest (which will be ignored if this is set). This can - for instance be used to get the message with ID 123 from - a channel. Note that if the message doesn't exist, `None` - will appear in its place, so that zipping the list of IDs - with the messages can match one-to-one. - - .. note:: - - At the time of writing, Telegram will **not** return - :tl:`MessageEmpty` for :tl:`InputMessageReplyTo` IDs that - failed (i.e. the message is not replying to any, or is - replying to a deleted message). This means that it is - **not** possible to match messages one-by-one, so be - careful if you use non-integers in this parameter. - - reverse (`bool`, optional): - If set to `True`, the messages will be returned in reverse - order (from oldest to newest, instead of the default newest - to oldest). This also means that the meaning of `offset_id` - and `offset_date` parameters is reversed, although they will - still be exclusive. `min_id` becomes equivalent to `offset_id` - instead of being `max_id` as well since messages are returned - in ascending order. - - You cannot use this if both `entity` and `ids` are `None`. - - reply_to (`int`, optional): - If set to a message ID, the messages that reply to this ID - will be returned. This feature is also known as comments in - posts of broadcast channels, or viewing threads in groups. - - This feature can only be used in broadcast channels and their - linked megagroups. Using it in a chat or private conversation - will result in ``telethon.errors.PeerIdInvalidError`` to occur. - - When using this parameter, the ``filter`` and ``search`` - parameters have no effect, since Telegram's API doesn't - support searching messages in replies. - - .. note:: - - This feature is used to get replies to a message in the - *discussion* group. If the same broadcast channel sends - a message and replies to it itself, that reply will not - be included in the results. - - scheduled (`bool`, optional): - If set to `True`, messages which are scheduled will be returned. - All other parameter will be ignored for this, except `entity`. - - Yields - Instances of `Message `. - - Example - .. code-block:: python - - # From most-recent to oldest - async for message in client.iter_messages(chat): - print(message.id, message.text) - - # From oldest to most-recent - async for message in client.iter_messages(chat, reverse=True): - print(message.id, message.text) - - # Filter by sender - async for message in client.iter_messages(chat, from_user='me'): - print(message.text) - - # Server-side search with fuzzy text - async for message in client.iter_messages(chat, search='hello'): - print(message.id) - - # Filter by message type: - from telethon.tl.types import InputMessagesFilterPhotos - async for message in client.iter_messages(chat, filter=InputMessagesFilterPhotos): - print(message.photo) - - # Getting comments from a post in a channel: - async for message in client.iter_messages(channel, reply_to=123): - print(message.chat.title, message.text) - """ - if ids is not None: - if not utils.is_list_like(ids): - ids = [ids] - - return _IDsIter( - client=self, - reverse=reverse, - wait_time=wait_time, - limit=len(ids), - entity=entity, - ids=ids - ) - - return _MessagesIter( + return _IDsIter( client=self, reverse=reverse, wait_time=wait_time, - limit=limit, + limit=len(ids), entity=entity, - offset_id=offset_id, - min_id=min_id, - max_id=max_id, - from_user=from_user, - offset_date=offset_date, - add_offset=add_offset, - filter=filter, - search=search, - reply_to=reply_to, - scheduled=scheduled + ids=ids ) - async def get_messages(self: 'TelegramClient', *args, **kwargs) -> 'hints.TotalList': - """ - Same as `iter_messages()`, but returns a - `TotalList ` instead. + return _MessagesIter( + client=self, + reverse=reverse, + wait_time=wait_time, + limit=limit, + entity=entity, + offset_id=offset_id, + min_id=min_id, + max_id=max_id, + from_user=from_user, + offset_date=offset_date, + add_offset=add_offset, + filter=filter, + search=search, + reply_to=reply_to, + scheduled=scheduled + ) - If the `limit` is not set, it will be 1 by default unless both - `min_id` **and** `max_id` are set (as *named* arguments), in - which case the entire range will be returned. - - This is so because any integer limit would be rather arbitrary and - it's common to only want to fetch one message, but if a range is - specified it makes sense that it should return the entirety of it. - - If `ids` is present in the *named* arguments and is not a list, - a single `Message ` will be - returned for convenience instead of a list. - - Example - .. code-block:: python - - # Get 0 photos and print the total to show how many photos there are - from telethon.tl.types import InputMessagesFilterPhotos - photos = await client.get_messages(chat, 0, filter=InputMessagesFilterPhotos) - print(photos.total) - - # Get all the photos - photos = await client.get_messages(chat, None, filter=InputMessagesFilterPhotos) - - # Get messages by ID: - message_1337 = await client.get_messages(chat, ids=1337) - """ - if len(args) == 1 and 'limit' not in kwargs: - if 'min_id' in kwargs and 'max_id' in kwargs: - kwargs['limit'] = None - else: - kwargs['limit'] = 1 - - it = self.iter_messages(*args, **kwargs) - - ids = kwargs.get('ids') - if ids and not utils.is_list_like(ids): - async for message in it: - return message - else: - # Iterator exhausted = empty, to handle InputMessageReplyTo - return None - - return await it.collect() - - get_messages.__signature__ = inspect.signature(iter_messages) - - # endregion - - # region Message sending/editing/deleting - - async def _get_comment_data( - self: 'TelegramClient', - entity: 'hints.EntityLike', - message: 'typing.Union[int, types.Message]' - ): - r = await self(functions.messages.GetDiscussionMessageRequest( - peer=entity, - msg_id=utils.get_message_id(message) - )) - m = r.messages[0] - chat = next(c for c in r.chats if c.id == m.peer_id.channel_id) - return utils.get_input_peer(chat), m.id - - async def send_message( - self: 'TelegramClient', - entity: 'hints.EntityLike', - message: 'hints.MessageLike' = '', - *, - reply_to: 'typing.Union[int, types.Message]' = None, - attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None, - parse_mode: typing.Optional[str] = (), - formatting_entities: typing.Optional[typing.List[types.TypeMessageEntity]] = None, - link_preview: bool = True, - file: 'typing.Union[hints.FileLike, typing.Sequence[hints.FileLike]]' = None, - thumb: 'hints.FileLike' = None, - force_document: bool = False, - clear_draft: bool = False, - buttons: 'hints.MarkupLike' = None, - silent: bool = None, - background: bool = None, - supports_streaming: bool = False, - schedule: 'hints.DateLike' = None, - comment_to: 'typing.Union[int, types.Message]' = None - ) -> 'types.Message': - """ - Sends a message to the specified user, chat or channel. - - The default parse mode is the same as the official applications - (a custom flavour of markdown). ``**bold**, `code` or __italic__`` - are available. In addition you can send ``[links](https://example.com)`` - and ``[mentions](@username)`` (or using IDs like in the Bot API: - ``[mention](tg://user?id=123456789)``) and ``pre`` blocks with three - backticks. - - Sending a ``/start`` command with a parameter (like ``?start=data``) - is also done through this method. Simply send ``'/start data'`` to - the bot. - - See also `Message.respond() ` - and `Message.reply() `. - - Arguments - entity (`entity`): - To who will it be sent. - - message (`str` | `Message `): - The message to be sent, or another message object to resend. - - The maximum length for a message is 35,000 bytes or 4,096 - characters. Longer messages will not be sliced automatically, - and you should slice them manually if the text to send is - longer than said length. - - reply_to (`int` | `Message `, optional): - Whether to reply to a message or not. If an integer is provided, - it should be the ID of the message that it should reply to. - - attributes (`list`, optional): - Optional attributes that override the inferred ones, like - :tl:`DocumentAttributeFilename` and so on. - - parse_mode (`object`, optional): - See the `TelegramClient.parse_mode - ` - property for allowed values. Markdown parsing will be used by - default. - - formatting_entities (`list`, optional): - A list of message formatting entities. When provided, the ``parse_mode`` is ignored. - - link_preview (`bool`, optional): - Should the link preview be shown? - - file (`file`, optional): - Sends a message with a file attached (e.g. a photo, - video, audio or document). The ``message`` may be empty. - - thumb (`str` | `bytes` | `file`, optional): - Optional JPEG thumbnail (for documents). **Telegram will - ignore this parameter** unless you pass a ``.jpg`` file! - The file must also be small in dimensions and in disk size. - Successful thumbnails were files below 20kB and 320x320px. - Width/height and dimensions/size ratios may be important. - For Telegram to accept a thumbnail, you must provide the - dimensions of the underlying media through ``attributes=`` - with :tl:`DocumentAttributesVideo` or by installing the - optional ``hachoir`` dependency. - - force_document (`bool`, optional): - Whether to send the given file as a document or not. - - clear_draft (`bool`, optional): - Whether the existing draft should be cleared or not. - - buttons (`list`, `custom.Button `, :tl:`KeyboardButton`): - The matrix (list of lists), row list or button to be shown - after sending the message. This parameter will only work if - you have signed in as a bot. You can also pass your own - :tl:`ReplyMarkup` here. - - All the following limits apply together: - - * There can be 100 buttons at most (any more are ignored). - * There can be 8 buttons per row at most (more are ignored). - * The maximum callback data per button is 64 bytes. - * The maximum data that can be embedded in total is just - over 4KB, shared between inline callback data and text. - - silent (`bool`, optional): - Whether the message should notify people in a broadcast - channel or not. Defaults to `False`, which means it will - notify them. Set it to `True` to alter this behaviour. - - background (`bool`, optional): - Whether the message should be send in background. - - supports_streaming (`bool`, optional): - Whether the sent video supports streaming or not. Note that - Telegram only recognizes as streamable some formats like MP4, - and others like AVI or MKV will not work. You should convert - these to MP4 before sending if you want them to be streamable. - Unsupported formats will result in ``VideoContentTypeError``. - - schedule (`hints.DateLike`, optional): - If set, the message won't send immediately, and instead - it will be scheduled to be automatically sent at a later - time. - - comment_to (`int` | `Message `, optional): - Similar to ``reply_to``, but replies in the linked group of a - broadcast channel instead (effectively leaving a "comment to" - the specified message). - - This parameter takes precedence over ``reply_to``. If there is - no linked chat, `telethon.errors.sgIdInvalidError` is raised. - - Returns - The sent `custom.Message `. - - Example - .. code-block:: python - - # Markdown is the default - await client.send_message('me', 'Hello **world**!') - - # Default to another parse mode - client.parse_mode = 'html' - - await client.send_message('me', 'Some bold and italic text') - await client.send_message('me', 'An URL') - # code and pre tags also work, but those break the documentation :) - await client.send_message('me', 'Mentions') - - # Explicit parse mode - # No parse mode by default - client.parse_mode = None - - # ...but here I want markdown - await client.send_message('me', 'Hello, **world**!', parse_mode='md') - - # ...and here I need HTML - await client.send_message('me', 'Hello, world!', parse_mode='html') - - # If you logged in as a bot account, you can send buttons - from telethon import events, Button - - @client.on(events.CallbackQuery) - async def callback(event): - await event.edit('Thank you for clicking {}!'.format(event.data)) - - # Single inline button - await client.send_message(chat, 'A single button, with "clk1" as data', - buttons=Button.inline('Click me', b'clk1')) - - # Matrix of inline buttons - await client.send_message(chat, 'Pick one from this grid', buttons=[ - [Button.inline('Left'), Button.inline('Right')], - [Button.url('Check this site!', 'https://example.com')] - ]) - - # Reply keyboard - await client.send_message(chat, 'Welcome', buttons=[ - Button.text('Thanks!', resize=True, single_use=True), - Button.request_phone('Send phone'), - Button.request_location('Send location') - ]) - - # Forcing replies or clearing buttons. - await client.send_message(chat, 'Reply to me', buttons=Button.force_reply()) - await client.send_message(chat, 'Bye Keyboard!', buttons=Button.clear()) - - # Scheduling a message to be sent after 5 minutes - from datetime import timedelta - await client.send_message(chat, 'Hi, future!', schedule=timedelta(minutes=5)) - """ - if file is not None: - return await self.send_file( - entity, file, caption=message, reply_to=reply_to, - attributes=attributes, parse_mode=parse_mode, - force_document=force_document, thumb=thumb, - buttons=buttons, clear_draft=clear_draft, silent=silent, - schedule=schedule, supports_streaming=supports_streaming, - formatting_entities=formatting_entities, - comment_to=comment_to, background=background - ) - - entity = await self.get_input_entity(entity) - if comment_to is not None: - entity, reply_to = await self._get_comment_data(entity, comment_to) - - if isinstance(message, types.Message): - if buttons is None: - markup = message.reply_markup - else: - markup = self.build_reply_markup(buttons) - - if silent is None: - silent = message.silent - - if (message.media and not isinstance( - message.media, types.MessageMediaWebPage)): - return await self.send_file( - entity, - message.media, - caption=message.message, - silent=silent, - background=background, - reply_to=reply_to, - buttons=markup, - formatting_entities=message.entities, - schedule=schedule - ) - - request = functions.messages.SendMessageRequest( - peer=entity, - message=message.message or '', - silent=silent, - background=background, - reply_to_msg_id=utils.get_message_id(reply_to), - reply_markup=markup, - entities=message.entities, - clear_draft=clear_draft, - no_webpage=not isinstance( - message.media, types.MessageMediaWebPage), - schedule_date=schedule - ) - message = message.message +async def get_messages(self: 'TelegramClient', *args, **kwargs) -> 'hints.TotalList': + if len(args) == 1 and 'limit' not in kwargs: + if 'min_id' in kwargs and 'max_id' in kwargs: + kwargs['limit'] = None else: - if formatting_entities is None: - message, formatting_entities = await self._parse_message_text(message, parse_mode) - if not message: - raise ValueError( - 'The message cannot be empty unless a file is provided' - ) + kwargs['limit'] = 1 - request = functions.messages.SendMessageRequest( - peer=entity, - message=message, - entities=formatting_entities, - no_webpage=not link_preview, - reply_to_msg_id=utils.get_message_id(reply_to), - clear_draft=clear_draft, - silent=silent, - background=background, - reply_markup=self.build_reply_markup(buttons), - schedule_date=schedule - ) + it = self.iter_messages(*args, **kwargs) - result = await self(request) - if isinstance(result, types.UpdateShortSentMessage): - message = types.Message( - id=result.id, - peer_id=await self._get_peer(entity), - message=message, - date=result.date, - out=result.out, - media=result.media, - entities=result.entities, - reply_markup=request.reply_markup, - ttl_period=result.ttl_period - ) - message._finish_init(self, {}, entity) + ids = kwargs.get('ids') + if ids and not utils.is_list_like(ids): + async for message in it: return message - - return self._get_response_message(request, result, entity) - - async def forward_messages( - self: 'TelegramClient', - entity: 'hints.EntityLike', - messages: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]', - from_peer: 'hints.EntityLike' = None, - *, - background: bool = None, - with_my_score: bool = None, - silent: bool = None, - as_album: bool = None, - schedule: 'hints.DateLike' = None - ) -> 'typing.Sequence[types.Message]': - """ - Forwards the given messages to the specified entity. - - If you want to "forward" a message without the forward header - (the "forwarded from" text), you should use `send_message` with - the original message instead. This will send a copy of it. - - See also `Message.forward_to() `. - - Arguments - entity (`entity`): - To which entity the message(s) will be forwarded. - - messages (`list` | `int` | `Message `): - The message(s) to forward, or their integer IDs. - - from_peer (`entity`): - If the given messages are integer IDs and not instances - of the ``Message`` class, this *must* be specified in - order for the forward to work. This parameter indicates - the entity from which the messages should be forwarded. - - silent (`bool`, optional): - Whether the message should notify people with sound or not. - Defaults to `False` (send with a notification sound unless - the person has the chat muted). Set it to `True` to alter - this behaviour. - - background (`bool`, optional): - Whether the message should be forwarded in background. - - with_my_score (`bool`, optional): - Whether forwarded should contain your game score. - - as_album (`bool`, optional): - This flag no longer has any effect. - - schedule (`hints.DateLike`, optional): - If set, the message(s) won't forward immediately, and - instead they will be scheduled to be automatically sent - at a later time. - - Returns - The list of forwarded `Message `, - or a single one if a list wasn't provided as input. - - Note that if all messages are invalid (i.e. deleted) the call - will fail with ``MessageIdInvalidError``. If only some are - invalid, the list will have `None` instead of those messages. - - Example - .. code-block:: python - - # a single one - await client.forward_messages(chat, message) - # or - await client.forward_messages(chat, message_id, from_chat) - # or - await message.forward_to(chat) - - # multiple - await client.forward_messages(chat, messages) - # or - await client.forward_messages(chat, message_ids, from_chat) - - # Forwarding as a copy - await client.send_message(chat, message) - """ - if as_album is not None: - warnings.warn('the as_album argument is deprecated and no longer has any effect') - - single = not utils.is_list_like(messages) - if single: - messages = (messages,) - - entity = await self.get_input_entity(entity) - - if from_peer: - from_peer = await self.get_input_entity(from_peer) - from_peer_id = await self.get_peer_id(from_peer) else: - from_peer_id = None + # Iterator exhausted = empty, to handle InputMessageReplyTo + return None - def get_key(m): - if isinstance(m, int): - if from_peer_id is not None: - return from_peer_id + return await it.collect() - raise ValueError('from_peer must be given if integer IDs are used') - elif isinstance(m, types.Message): - return m.chat_id - else: - raise TypeError('Cannot forward messages of type {}'.format(type(m))) - sent = [] - for _chat_id, chunk in itertools.groupby(messages, key=get_key): - chunk = list(chunk) - if isinstance(chunk[0], int): - chat = from_peer - else: - chat = await chunk[0].get_input_chat() - chunk = [m.id for m in chunk] +async def _get_comment_data( + self: 'TelegramClient', + entity: 'hints.EntityLike', + message: 'typing.Union[int, types.Message]' +): + r = await self(functions.messages.GetDiscussionMessageRequest( + peer=entity, + msg_id=utils.get_message_id(message) + )) + m = r.messages[0] + chat = next(c for c in r.chats if c.id == m.peer_id.channel_id) + return utils.get_input_peer(chat), m.id - req = functions.messages.ForwardMessagesRequest( - from_peer=chat, - id=chunk, - to_peer=entity, +async def send_message( + self: 'TelegramClient', + entity: 'hints.EntityLike', + message: 'hints.MessageLike' = '', + *, + reply_to: 'typing.Union[int, types.Message]' = None, + attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None, + parse_mode: typing.Optional[str] = (), + formatting_entities: typing.Optional[typing.List[types.TypeMessageEntity]] = None, + link_preview: bool = True, + file: 'typing.Union[hints.FileLike, typing.Sequence[hints.FileLike]]' = None, + thumb: 'hints.FileLike' = None, + force_document: bool = False, + clear_draft: bool = False, + buttons: 'hints.MarkupLike' = None, + silent: bool = None, + background: bool = None, + supports_streaming: bool = False, + schedule: 'hints.DateLike' = None, + comment_to: 'typing.Union[int, types.Message]' = None +) -> 'types.Message': + if file is not None: + return await self.send_file( + entity, file, caption=message, reply_to=reply_to, + attributes=attributes, parse_mode=parse_mode, + force_document=force_document, thumb=thumb, + buttons=buttons, clear_draft=clear_draft, silent=silent, + schedule=schedule, supports_streaming=supports_streaming, + formatting_entities=formatting_entities, + comment_to=comment_to, background=background + ) + + entity = await self.get_input_entity(entity) + if comment_to is not None: + entity, reply_to = await self._get_comment_data(entity, comment_to) + + if isinstance(message, types.Message): + if buttons is None: + markup = message.reply_markup + else: + markup = self.build_reply_markup(buttons) + + if silent is None: + silent = message.silent + + if (message.media and not isinstance( + message.media, types.MessageMediaWebPage)): + return await self.send_file( + entity, + message.media, + caption=message.message, silent=silent, background=background, - with_my_score=with_my_score, - schedule_date=schedule + reply_to=reply_to, + buttons=markup, + formatting_entities=message.entities, + schedule=schedule ) - result = await self(req) - sent.extend(self._get_response_message(req, result, entity)) - return sent[0] if single else sent - - async def edit_message( - self: 'TelegramClient', - entity: 'typing.Union[hints.EntityLike, types.Message]', - message: 'hints.MessageLike' = None, - text: str = None, - *, - parse_mode: str = (), - attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None, - formatting_entities: typing.Optional[typing.List[types.TypeMessageEntity]] = None, - link_preview: bool = True, - file: 'hints.FileLike' = None, - thumb: 'hints.FileLike' = None, - force_document: bool = False, - buttons: 'hints.MarkupLike' = None, - supports_streaming: bool = False, - schedule: 'hints.DateLike' = None - ) -> 'types.Message': - """ - Edits the given message to change its text or media. - - See also `Message.edit() `. - - Arguments - entity (`entity` | `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. - - You may also pass a :tl:`InputBotInlineMessageID`, - which is the only way to edit messages that were sent - after the user selects an inline query result. - - message (`int` | `Message ` | `str`): - The ID of the message (or `Message - ` itself) to be edited. - If the `entity` was a `Message - `, then this message - will be treated as the new text. - - text (`str`, optional): - The new text of the message. Does nothing if the `entity` - was a `Message `. - - parse_mode (`object`, optional): - See the `TelegramClient.parse_mode - ` - property for allowed values. Markdown parsing will be used by - default. - - attributes (`list`, optional): - Optional attributes that override the inferred ones, like - :tl:`DocumentAttributeFilename` and so on. - - formatting_entities (`list`, optional): - A list of message formatting entities. When provided, the ``parse_mode`` is ignored. - - link_preview (`bool`, optional): - Should the link preview be shown? - - file (`str` | `bytes` | `file` | `media`, optional): - The file object that should replace the existing media - in the message. - - thumb (`str` | `bytes` | `file`, optional): - Optional JPEG thumbnail (for documents). **Telegram will - ignore this parameter** unless you pass a ``.jpg`` file! - The file must also be small in dimensions and in disk size. - Successful thumbnails were files below 20kB and 320x320px. - Width/height and dimensions/size ratios may be important. - For Telegram to accept a thumbnail, you must provide the - dimensions of the underlying media through ``attributes=`` - with :tl:`DocumentAttributesVideo` or by installing the - optional ``hachoir`` dependency. - - force_document (`bool`, optional): - Whether to send the given file as a document or not. - - buttons (`list`, `custom.Button `, :tl:`KeyboardButton`): - The matrix (list of lists), row list or button to be shown - after sending the message. This parameter will only work if - you have signed in as a bot. You can also pass your own - :tl:`ReplyMarkup` here. - - supports_streaming (`bool`, optional): - Whether the sent video supports streaming or not. Note that - Telegram only recognizes as streamable some formats like MP4, - and others like AVI or MKV will not work. You should convert - these to MP4 before sending if you want them to be streamable. - Unsupported formats will result in ``VideoContentTypeError``. - - schedule (`hints.DateLike`, optional): - If set, the message won't be edited immediately, and instead - it will be scheduled to be automatically edited at a later - time. - - Note that this parameter will have no effect if you are - trying to edit a message that was sent via inline bots. - - Returns - The edited `Message `, - unless `entity` was a :tl:`InputBotInlineMessageID` in which - case this method returns a boolean. - - Raises - ``MessageAuthorRequiredError`` if you're not the author of the - message but tried editing it anyway. - - ``MessageNotModifiedError`` if the contents of the message were - not modified at all. - - ``MessageIdInvalidError`` if the ID of the message is invalid - (the ID itself may be correct, but the message with that ID - cannot be edited). For example, when trying to edit messages - with a reply markup (or clear markup) this error will be raised. - - Example - .. code-block:: python - - message = await client.send_message(chat, 'hello') - - await client.edit_message(chat, message, 'hello!') - # or - await client.edit_message(chat, message.id, 'hello!!') - # or - await client.edit_message(message, 'hello!!!') - """ - if isinstance(entity, types.InputBotInlineMessageID): - text = text or message - message = entity - elif isinstance(entity, types.Message): - text = message # Shift the parameters to the right - message = entity - entity = entity.peer_id - - if formatting_entities is None: - text, formatting_entities = await self._parse_message_text(text, parse_mode) - file_handle, media, image = await self._file_to_media(file, - supports_streaming=supports_streaming, - thumb=thumb, - attributes=attributes, - force_document=force_document) - - if isinstance(entity, types.InputBotInlineMessageID): - request = functions.messages.EditInlineBotMessageRequest( - id=entity, - message=text, - no_webpage=not link_preview, - entities=formatting_entities, - media=media, - reply_markup=self.build_reply_markup(buttons) - ) - # Invoke `messages.editInlineBotMessage` from the right datacenter. - # Otherwise, Telegram will error with `MESSAGE_ID_INVALID` and do nothing. - exported = self.session.dc_id != entity.dc_id - if exported: - try: - sender = await self._borrow_exported_sender(entity.dc_id) - return await self._call(sender, request) - finally: - await self._return_exported_sender(sender) - else: - return await self(request) - - entity = await self.get_input_entity(entity) - request = functions.messages.EditMessageRequest( + request = functions.messages.SendMessageRequest( peer=entity, - id=utils.get_message_id(message), + message=message.message or '', + silent=silent, + background=background, + reply_to_msg_id=utils.get_message_id(reply_to), + reply_markup=markup, + entities=message.entities, + clear_draft=clear_draft, + no_webpage=not isinstance( + message.media, types.MessageMediaWebPage), + schedule_date=schedule + ) + message = message.message + else: + if formatting_entities is None: + message, formatting_entities = await self._parse_message_text(message, parse_mode) + if not message: + raise ValueError( + 'The message cannot be empty unless a file is provided' + ) + + request = functions.messages.SendMessageRequest( + peer=entity, + message=message, + entities=formatting_entities, + no_webpage=not link_preview, + reply_to_msg_id=utils.get_message_id(reply_to), + clear_draft=clear_draft, + silent=silent, + background=background, + reply_markup=self.build_reply_markup(buttons), + schedule_date=schedule + ) + + result = await self(request) + if isinstance(result, types.UpdateShortSentMessage): + message = types.Message( + id=result.id, + peer_id=await self._get_peer(entity), + message=message, + date=result.date, + out=result.out, + media=result.media, + entities=result.entities, + reply_markup=request.reply_markup, + ttl_period=result.ttl_period + ) + message._finish_init(self, {}, entity) + return message + + return self._get_response_message(request, result, entity) + +async def forward_messages( + self: 'TelegramClient', + entity: 'hints.EntityLike', + messages: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]', + from_peer: 'hints.EntityLike' = None, + *, + background: bool = None, + with_my_score: bool = None, + silent: bool = None, + as_album: bool = None, + schedule: 'hints.DateLike' = None +) -> 'typing.Sequence[types.Message]': + if as_album is not None: + warnings.warn('the as_album argument is deprecated and no longer has any effect') + + single = not utils.is_list_like(messages) + if single: + messages = (messages,) + + entity = await self.get_input_entity(entity) + + if from_peer: + from_peer = await self.get_input_entity(from_peer) + from_peer_id = await self.get_peer_id(from_peer) + else: + from_peer_id = None + + def get_key(m): + if isinstance(m, int): + if from_peer_id is not None: + return from_peer_id + + raise ValueError('from_peer must be given if integer IDs are used') + elif isinstance(m, types.Message): + return m.chat_id + else: + raise TypeError('Cannot forward messages of type {}'.format(type(m))) + + sent = [] + for _chat_id, chunk in itertools.groupby(messages, key=get_key): + chunk = list(chunk) + if isinstance(chunk[0], int): + chat = from_peer + else: + chat = await chunk[0].get_input_chat() + chunk = [m.id for m in chunk] + + req = functions.messages.ForwardMessagesRequest( + from_peer=chat, + id=chunk, + to_peer=entity, + silent=silent, + background=background, + with_my_score=with_my_score, + schedule_date=schedule + ) + result = await self(req) + sent.extend(self._get_response_message(req, result, entity)) + + return sent[0] if single else sent + +async def edit_message( + self: 'TelegramClient', + entity: 'typing.Union[hints.EntityLike, types.Message]', + message: 'hints.MessageLike' = None, + text: str = None, + *, + parse_mode: str = (), + attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None, + formatting_entities: typing.Optional[typing.List[types.TypeMessageEntity]] = None, + link_preview: bool = True, + file: 'hints.FileLike' = None, + thumb: 'hints.FileLike' = None, + force_document: bool = False, + buttons: 'hints.MarkupLike' = None, + supports_streaming: bool = False, + schedule: 'hints.DateLike' = None +) -> 'types.Message': + if isinstance(entity, types.InputBotInlineMessageID): + text = text or message + message = entity + elif isinstance(entity, types.Message): + text = message # Shift the parameters to the right + message = entity + entity = entity.peer_id + + if formatting_entities is None: + text, formatting_entities = await self._parse_message_text(text, parse_mode) + file_handle, media, image = await self._file_to_media(file, + supports_streaming=supports_streaming, + thumb=thumb, + attributes=attributes, + force_document=force_document) + + if isinstance(entity, types.InputBotInlineMessageID): + request = functions.messages.EditInlineBotMessageRequest( + id=entity, message=text, no_webpage=not link_preview, entities=formatting_entities, media=media, - reply_markup=self.build_reply_markup(buttons), - schedule_date=schedule + reply_markup=self.build_reply_markup(buttons) ) - msg = self._get_response_message(request, await self(request), entity) - return msg - - async def delete_messages( - self: 'TelegramClient', - entity: 'hints.EntityLike', - message_ids: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]', - *, - revoke: bool = True) -> 'typing.Sequence[types.messages.AffectedMessages]': - """ - Deletes the given messages, optionally "for everyone". - - See also `Message.delete() `. - - .. warning:: - - This method does **not** validate that the message IDs belong - to the chat that you passed! It's possible for the method to - delete messages from different private chats and small group - chats at once, so make sure to pass the right IDs. - - Arguments - entity (`entity`): - From who the message will be deleted. This can actually - be `None` for normal chats, but **must** be present - for channels and megagroups. - - message_ids (`list` | `int` | `Message `): - The IDs (or ID) or messages to be deleted. - - revoke (`bool`, optional): - Whether the message should be deleted for everyone or not. - By default it has the opposite behaviour of official clients, - and it will delete the message for everyone. - - `Since 24 March 2019 - `_, you can - also revoke messages of any age (i.e. messages sent long in - the past) the *other* person sent in private conversations - (and of course your messages too). - - Disabling this has no effect on channels or megagroups, - since it will unconditionally delete the message for everyone. - - Returns - A list of :tl:`AffectedMessages`, each item being the result - for the delete calls of the messages in chunks of 100 each. - - Example - .. code-block:: python - - await client.delete_messages(chat, messages) - """ - if not utils.is_list_like(message_ids): - message_ids = (message_ids,) - - message_ids = ( - m.id if isinstance(m, ( - types.Message, types.MessageService, types.MessageEmpty)) - else int(m) for m in message_ids - ) - - if entity: - entity = await self.get_input_entity(entity) - ty = helpers._entity_type(entity) + # Invoke `messages.editInlineBotMessage` from the right datacenter. + # Otherwise, Telegram will error with `MESSAGE_ID_INVALID` and do nothing. + exported = self.session.dc_id != entity.dc_id + if exported: + try: + sender = await self._borrow_exported_sender(entity.dc_id) + return await self._call(sender, request) + finally: + await self._return_exported_sender(sender) else: - # no entity (None), set a value that's not a channel for private delete - ty = helpers._EntityType.USER + return await self(request) - if ty == helpers._EntityType.CHANNEL: - return await self([functions.channels.DeleteMessagesRequest( - entity, list(c)) for c in utils.chunks(message_ids)]) + entity = await self.get_input_entity(entity) + request = functions.messages.EditMessageRequest( + peer=entity, + id=utils.get_message_id(message), + message=text, + no_webpage=not link_preview, + entities=formatting_entities, + media=media, + reply_markup=self.build_reply_markup(buttons), + schedule_date=schedule + ) + msg = self._get_response_message(request, await self(request), entity) + return msg + +async def delete_messages( + self: 'TelegramClient', + entity: 'hints.EntityLike', + message_ids: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]', + *, + revoke: bool = True) -> 'typing.Sequence[types.messages.AffectedMessages]': + if not utils.is_list_like(message_ids): + message_ids = (message_ids,) + + message_ids = ( + m.id if isinstance(m, ( + types.Message, types.MessageService, types.MessageEmpty)) + else int(m) for m in message_ids + ) + + if entity: + entity = await self.get_input_entity(entity) + ty = helpers._entity_type(entity) + else: + # no entity (None), set a value that's not a channel for private delete + ty = helpers._EntityType.USER + + if ty == helpers._EntityType.CHANNEL: + return await self([functions.channels.DeleteMessagesRequest( + entity, list(c)) for c in utils.chunks(message_ids)]) + else: + return await self([functions.messages.DeleteMessagesRequest( + list(c), revoke) for c in utils.chunks(message_ids)]) + +async def send_read_acknowledge( + self: 'TelegramClient', + entity: 'hints.EntityLike', + message: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]' = None, + *, + max_id: int = None, + clear_mentions: bool = False) -> bool: + if max_id is None: + if not message: + max_id = 0 else: - return await self([functions.messages.DeleteMessagesRequest( - list(c), revoke) for c in utils.chunks(message_ids)]) + if utils.is_list_like(message): + max_id = max(msg.id for msg in message) + else: + max_id = message.id - # endregion - - # region Miscellaneous - - async def send_read_acknowledge( - self: 'TelegramClient', - entity: 'hints.EntityLike', - message: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]' = None, - *, - max_id: int = None, - clear_mentions: bool = False) -> bool: - """ - Marks messages as read and optionally clears mentions. - - This effectively marks a message as read (or more than one) in the - given conversation. - - If neither message nor maximum ID are provided, all messages will be - marked as read by assuming that ``max_id = 0``. - - If a message or maximum ID is provided, all the messages up to and - including such ID will be marked as read (for all messages whose ID - ≤ max_id). - - See also `Message.mark_read() `. - - Arguments - entity (`entity`): - The chat where these messages are located. - - message (`list` | `Message `): - Either a list of messages or a single message. - - max_id (`int`): - Until which message should the read acknowledge be sent for. - This has priority over the ``message`` parameter. - - clear_mentions (`bool`): - Whether the mention badge should be cleared (so that - there are no more mentions) or not for the given entity. - - If no message is provided, this will be the only action - taken. - - Example - .. code-block:: python - - # using a Message object - await client.send_read_acknowledge(chat, message) - # ...or using the int ID of a Message - await client.send_read_acknowledge(chat, message_id) - # ...or passing a list of messages to mark as read - await client.send_read_acknowledge(chat, messages) - """ + entity = await self.get_input_entity(entity) + if clear_mentions: + await self(functions.messages.ReadMentionsRequest(entity)) if max_id is None: - if not message: - max_id = 0 - else: - if utils.is_list_like(message): - max_id = max(msg.id for msg in message) - else: - max_id = message.id + return True - entity = await self.get_input_entity(entity) - if clear_mentions: - await self(functions.messages.ReadMentionsRequest(entity)) - if max_id is None: - return True + if max_id is not None: + if helpers._entity_type(entity) == helpers._EntityType.CHANNEL: + return await self(functions.channels.ReadHistoryRequest( + utils.get_input_channel(entity), max_id=max_id)) + else: + return await self(functions.messages.ReadHistoryRequest( + entity, max_id=max_id)) - if max_id is not None: - if helpers._entity_type(entity) == helpers._EntityType.CHANNEL: - return await self(functions.channels.ReadHistoryRequest( - utils.get_input_channel(entity), max_id=max_id)) - else: - return await self(functions.messages.ReadHistoryRequest( - entity, max_id=max_id)) + return False - return False +async def pin_message( + self: 'TelegramClient', + entity: 'hints.EntityLike', + message: 'typing.Optional[hints.MessageIDLike]', + *, + notify: bool = False, + pm_oneside: bool = False +): + return await self._pin(entity, message, unpin=False, notify=notify, pm_oneside=pm_oneside) - async def pin_message( - self: 'TelegramClient', - entity: 'hints.EntityLike', - message: 'typing.Optional[hints.MessageIDLike]', - *, - notify: bool = False, - pm_oneside: bool = False - ): - """ - Pins a message in a chat. +async def unpin_message( + self: 'TelegramClient', + entity: 'hints.EntityLike', + message: 'typing.Optional[hints.MessageIDLike]' = None, + *, + notify: bool = False +): + return await self._pin(entity, message, unpin=True, notify=notify) - The default behaviour is to *not* notify members, unlike the - official applications. +async def _pin(self, entity, message, *, unpin, notify=False, pm_oneside=False): + message = utils.get_message_id(message) or 0 + entity = await self.get_input_entity(entity) + if message <= 0: # old behaviour accepted negative IDs to unpin + await self(functions.messages.UnpinAllMessagesRequest(entity)) + return - See also `Message.pin() `. + request = functions.messages.UpdatePinnedMessageRequest( + peer=entity, + id=message, + silent=not notify, + unpin=unpin, + pm_oneside=pm_oneside + ) + result = await self(request) - Arguments - entity (`entity`): - The chat where the message should be pinned. + # Unpinning does not produce a service message. + # Pinning a message that was already pinned also produces no service message. + # Pinning a message in your own chat does not produce a service message, + # but pinning on a private conversation with someone else does. + if unpin or not result.updates: + return - message (`int` | `Message `): - The message or the message ID to pin. If it's - `None`, all messages will be unpinned instead. - - notify (`bool`, optional): - Whether the pin should notify people or not. - - pm_oneside (`bool`, optional): - Whether the message should be pinned for everyone or not. - By default it has the opposite behaviour of official clients, - and it will pin the message for both sides, in private chats. - - Example - .. code-block:: python - - # Send and pin a message to annoy everyone - message = await client.send_message(chat, 'Pinotifying is fun!') - await client.pin_message(chat, message, notify=True) - """ - return await self._pin(entity, message, unpin=False, notify=notify, pm_oneside=pm_oneside) - - async def unpin_message( - self: 'TelegramClient', - entity: 'hints.EntityLike', - message: 'typing.Optional[hints.MessageIDLike]' = None, - *, - notify: bool = False - ): - """ - Unpins a message in a chat. - - If no message ID is specified, all pinned messages will be unpinned. - - See also `Message.unpin() `. - - Arguments - entity (`entity`): - The chat where the message should be pinned. - - message (`int` | `Message `): - The message or the message ID to unpin. If it's - `None`, all messages will be unpinned instead. - - Example - .. code-block:: python - - # Unpin all messages from a chat - await client.unpin_message(chat) - """ - return await self._pin(entity, message, unpin=True, notify=notify) - - async def _pin(self, entity, message, *, unpin, notify=False, pm_oneside=False): - message = utils.get_message_id(message) or 0 - entity = await self.get_input_entity(entity) - if message <= 0: # old behaviour accepted negative IDs to unpin - await self(functions.messages.UnpinAllMessagesRequest(entity)) - return - - request = functions.messages.UpdatePinnedMessageRequest( - peer=entity, - id=message, - silent=not notify, - unpin=unpin, - pm_oneside=pm_oneside - ) - result = await self(request) - - # Unpinning does not produce a service message. - # Pinning a message that was already pinned also produces no service message. - # Pinning a message in your own chat does not produce a service message, - # but pinning on a private conversation with someone else does. - if unpin or not result.updates: - return - - # Pinning a message that doesn't exist would RPC-error earlier - return self._get_response_message(request, result, entity) - - # endregion - - # endregion + # Pinning a message that doesn't exist would RPC-error earlier + return self._get_response_message(request, result, entity) diff --git a/telethon/client/telegrambaseclient.py b/telethon/client/telegrambaseclient.py index 494daf9c..79ea85b3 100644 --- a/telethon/client/telegrambaseclient.py +++ b/telethon/client/telegrambaseclient.py @@ -64,812 +64,538 @@ class _ExportState: # TODO How hard would it be to support both `trio` and `asyncio`? -class TelegramBaseClient(abc.ABC): - """ - This is the abstract base class for the client. It defines some - basic stuff like connecting, switching data center, etc, and - leaves the `__call__` unimplemented. - Arguments - session (`str` | `telethon.sessions.abstract.Session`, `None`): - The file name of the session file to be used if a string is - given (it may be a full path), or the Session instance to be - used otherwise. If it's `None`, the session will not be saved, - and you should call :meth:`.log_out()` when you're done. - Note that if you pass a string it will be a file in the current - working directory, although you can also pass absolute paths. +def init( + self: 'TelegramClient', + session: 'typing.Union[str, Session]', + api_id: int, + api_hash: str, + *, + connection: 'typing.Type[Connection]' = ConnectionTcpFull, + use_ipv6: bool = False, + proxy: typing.Union[tuple, dict] = None, + local_addr: typing.Union[str, tuple] = None, + timeout: int = 10, + request_retries: int = 5, + connection_retries: int = 5, + retry_delay: int = 1, + auto_reconnect: bool = True, + sequential_updates: bool = False, + flood_sleep_threshold: int = 60, + raise_last_call_error: bool = False, + device_model: str = None, + system_version: str = None, + app_version: str = None, + lang_code: str = 'en', + system_lang_code: str = 'en', + loop: asyncio.AbstractEventLoop = None, + base_logger: typing.Union[str, logging.Logger] = None, + receive_updates: bool = True +): + if not api_id or not api_hash: + raise ValueError( + "Your API ID or Hash cannot be empty or None. " + "Refer to telethon.rtfd.io for more information.") - The session file contains enough information for you to login - without re-sending the code, so if you have to enter the code - more than once, maybe you're changing the working directory, - renaming or removing the file, or using random names. + self._use_ipv6 = use_ipv6 - api_id (`int` | `str`): - The API ID you obtained from https://my.telegram.org. + if isinstance(base_logger, str): + base_logger = logging.getLogger(base_logger) + elif not isinstance(base_logger, logging.Logger): + base_logger = _base_log - api_hash (`str`): - The API hash you obtained from https://my.telegram.org. + class _Loggers(dict): + def __missing__(self, key): + if key.startswith("telethon."): + key = key.split('.', maxsplit=1)[1] - connection (`telethon.network.connection.common.Connection`, optional): - The connection instance to be used when creating a new connection - to the servers. It **must** be a type. + return base_logger.getChild(key) - Defaults to `telethon.network.connection.tcpfull.ConnectionTcpFull`. + self._log = _Loggers() - use_ipv6 (`bool`, optional): - Whether to connect to the servers through IPv6 or not. - By default this is `False` as IPv6 support is not - too widespread yet. - - proxy (`tuple` | `list` | `dict`, optional): - An iterable consisting of the proxy info. If `connection` is - one of `MTProxy`, then it should contain MTProxy credentials: - ``('hostname', port, 'secret')``. Otherwise, it's meant to store - function parameters for PySocks, like ``(type, 'hostname', port)``. - See https://github.com/Anorov/PySocks#usage-1 for more. - - local_addr (`str` | `tuple`, optional): - Local host address (and port, optionally) used to bind the socket to locally. - You only need to use this if you have multiple network cards and - want to use a specific one. - - timeout (`int` | `float`, optional): - The timeout in seconds to be used when connecting. - This is **not** the timeout to be used when ``await``'ing for - invoked requests, and you should use ``asyncio.wait`` or - ``asyncio.wait_for`` for that. - - request_retries (`int` | `None`, optional): - How many times a request should be retried. Request are retried - when Telegram is having internal issues (due to either - ``errors.ServerError`` or ``errors.RpcCallFailError``), - when there is a ``errors.FloodWaitError`` less than - `flood_sleep_threshold`, or when there's a migrate error. - - May take a negative or `None` value for infinite retries, but - this is not recommended, since some requests can always trigger - a call fail (such as searching for messages). - - connection_retries (`int` | `None`, optional): - How many times the reconnection should retry, either on the - initial connection or when Telegram disconnects us. May be - set to a negative or `None` value for infinite retries, but - this is not recommended, since the program can get stuck in an - infinite loop. - - retry_delay (`int` | `float`, optional): - The delay in seconds to sleep between automatic reconnections. - - auto_reconnect (`bool`, optional): - Whether reconnection should be retried `connection_retries` - times automatically if Telegram disconnects us or not. - - sequential_updates (`bool`, optional): - By default every incoming update will create a new task, so - you can handle several updates in parallel. Some scripts need - the order in which updates are processed to be sequential, and - this setting allows them to do so. - - If set to `True`, incoming updates will be put in a queue - and processed sequentially. This means your event handlers - should *not* perform long-running operations since new - updates are put inside of an unbounded queue. - - flood_sleep_threshold (`int` | `float`, optional): - The threshold below which the library should automatically - sleep on flood wait and slow mode wait errors (inclusive). For instance, if a - ``FloodWaitError`` for 17s occurs and `flood_sleep_threshold` - is 20s, the library will ``sleep`` automatically. If the error - was for 21s, it would ``raise FloodWaitError`` instead. Values - larger than a day (like ``float('inf')``) will be changed to a day. - - raise_last_call_error (`bool`, optional): - When API calls fail in a way that causes Telethon to retry - automatically, should the RPC error of the last attempt be raised - instead of a generic ValueError. This is mostly useful for - detecting when Telegram has internal issues. - - device_model (`str`, optional): - "Device model" to be sent when creating the initial connection. - Defaults to 'PC (n)bit' derived from ``platform.uname().machine``, or its direct value if unknown. - - system_version (`str`, optional): - "System version" to be sent when creating the initial connection. - Defaults to ``platform.uname().release`` stripped of everything ahead of -. - - app_version (`str`, optional): - "App version" to be sent when creating the initial connection. - Defaults to `telethon.version.__version__`. - - lang_code (`str`, optional): - "Language code" to be sent when creating the initial connection. - Defaults to ``'en'``. - - system_lang_code (`str`, optional): - "System lang code" to be sent when creating the initial connection. - Defaults to `lang_code`. - - loop (`asyncio.AbstractEventLoop`, optional): - Asyncio event loop to use. Defaults to `asyncio.get_event_loop()`. - This argument is ignored. - - base_logger (`str` | `logging.Logger`, optional): - Base logger name or instance to use. - If a `str` is given, it'll be passed to `logging.getLogger()`. If a - `logging.Logger` is given, it'll be used directly. If something - else or nothing is given, the default logger will be used. - - receive_updates (`bool`, optional): - Whether the client will receive updates or not. By default, updates - will be received from Telegram as they occur. - - Turning this off means that Telegram will not send updates at all - so event handlers, conversations, and QR login will not work. - However, certain scripts don't need updates, so this will reduce - the amount of bandwidth used. - """ - - # Current TelegramClient version - __version__ = version.__version__ - - # Cached server configuration (with .dc_options), can be "global" - _config = None - _cdn_config = None - - # region Initialization - - def __init__( - self: 'TelegramClient', - session: 'typing.Union[str, Session]', - api_id: int, - api_hash: str, - *, - connection: 'typing.Type[Connection]' = ConnectionTcpFull, - use_ipv6: bool = False, - proxy: typing.Union[tuple, dict] = None, - local_addr: typing.Union[str, tuple] = None, - timeout: int = 10, - request_retries: int = 5, - connection_retries: int = 5, - retry_delay: int = 1, - auto_reconnect: bool = True, - sequential_updates: bool = False, - flood_sleep_threshold: int = 60, - raise_last_call_error: bool = False, - device_model: str = None, - system_version: str = None, - app_version: str = None, - lang_code: str = 'en', - system_lang_code: str = 'en', - loop: asyncio.AbstractEventLoop = None, - base_logger: typing.Union[str, logging.Logger] = None, - receive_updates: bool = True - ): - if not api_id or not api_hash: - raise ValueError( - "Your API ID or Hash cannot be empty or None. " - "Refer to telethon.rtfd.io for more information.") - - self._use_ipv6 = use_ipv6 - - if isinstance(base_logger, str): - base_logger = logging.getLogger(base_logger) - elif not isinstance(base_logger, logging.Logger): - base_logger = _base_log - - class _Loggers(dict): - def __missing__(self, key): - if key.startswith("telethon."): - key = key.split('.', maxsplit=1)[1] - - return base_logger.getChild(key) - - self._log = _Loggers() - - # Determine what session object we have - if isinstance(session, str) or session is None: - try: - session = SQLiteSession(session) - except ImportError: - import warnings - warnings.warn( - 'The sqlite3 module is not available under this ' - 'Python installation and no custom session ' - 'instance was given; using MemorySession.\n' - 'You will need to re-login every time unless ' - 'you use another session storage' - ) - session = MemorySession() - elif not isinstance(session, Session): - raise TypeError( - 'The given session must be a str or a Session instance.' + # Determine what session object we have + if isinstance(session, str) or session is None: + try: + session = SQLiteSession(session) + except ImportError: + import warnings + warnings.warn( + 'The sqlite3 module is not available under this ' + 'Python installation and no custom session ' + 'instance was given; using MemorySession.\n' + 'You will need to re-login every time unless ' + 'you use another session storage' ) - - # ':' in session.server_address is True if it's an IPv6 address - if (not session.server_address or - (':' in session.server_address) != use_ipv6): - session.set_dc( - DEFAULT_DC_ID, - DEFAULT_IPV6_IP if self._use_ipv6 else DEFAULT_IPV4_IP, - DEFAULT_PORT - ) - - self.flood_sleep_threshold = flood_sleep_threshold - - # TODO Use AsyncClassWrapper(session) - # ChatGetter and SenderGetter can use the in-memory _entity_cache - # to avoid network access and the need for await in session files. - # - # The session files only wants the entities to persist - # them to disk, and to save additional useful information. - # TODO Session should probably return all cached - # info of entities, not just the input versions - self.session = session - self._entity_cache = EntityCache() - self.api_id = int(api_id) - self.api_hash = api_hash - - # Current proxy implementation requires `sock_connect`, and some - # event loops lack this method. If the current loop is missing it, - # bail out early and suggest an alternative. - # - # TODO A better fix is obviously avoiding the use of `sock_connect` - # - # See https://github.com/LonamiWebs/Telethon/issues/1337 for details. - if not callable(getattr(self.loop, 'sock_connect', None)): - raise TypeError( - 'Event loop of type {} lacks `sock_connect`, which is needed to use proxies.\n\n' - 'Change the event loop in use to use proxies:\n' - '# https://github.com/LonamiWebs/Telethon/issues/1337\n' - 'import asyncio\n' - 'asyncio.set_event_loop(asyncio.SelectorEventLoop())'.format( - self.loop.__class__.__name__ - ) - ) - - if local_addr is not None: - if use_ipv6 is False and ':' in local_addr: - raise TypeError( - 'A local IPv6 address must only be used with `use_ipv6=True`.' - ) - elif use_ipv6 is True and ':' not in local_addr: - raise TypeError( - '`use_ipv6=True` must only be used with a local IPv6 address.' - ) - - self._raise_last_call_error = raise_last_call_error - - self._request_retries = request_retries - self._connection_retries = connection_retries - self._retry_delay = retry_delay or 0 - self._proxy = proxy - self._local_addr = local_addr - self._timeout = timeout - self._auto_reconnect = auto_reconnect - - assert isinstance(connection, type) - self._connection = connection - init_proxy = None if not issubclass(connection, TcpMTProxy) else \ - types.InputClientProxy(*connection.address_info(proxy)) - - # Used on connection. Capture the variables in a lambda since - # exporting clients need to create this InvokeWithLayerRequest. - system = platform.uname() - - if system.machine in ('x86_64', 'AMD64'): - default_device_model = 'PC 64bit' - elif system.machine in ('i386','i686','x86'): - default_device_model = 'PC 32bit' - else: - default_device_model = system.machine - default_system_version = re.sub(r'-.+','',system.release) - - self._init_request = functions.InitConnectionRequest( - api_id=self.api_id, - device_model=device_model or default_device_model or 'Unknown', - system_version=system_version or default_system_version or '1.0', - app_version=app_version or self.__version__, - lang_code=lang_code, - system_lang_code=system_lang_code, - lang_pack='', # "langPacks are for official apps only" - query=None, - proxy=init_proxy + session = MemorySession() + elif not isinstance(session, Session): + raise TypeError( + 'The given session must be a str or a Session instance.' ) - self._sender = MTProtoSender( - self.session.auth_key, - loggers=self._log, - retries=self._connection_retries, - delay=self._retry_delay, - auto_reconnect=self._auto_reconnect, - connect_timeout=self._timeout, - auth_key_callback=self._auth_key_callback, - update_callback=self._handle_update, - auto_reconnect_callback=self._handle_auto_reconnect + # ':' in session.server_address is True if it's an IPv6 address + if (not session.server_address or + (':' in session.server_address) != use_ipv6): + session.set_dc( + DEFAULT_DC_ID, + DEFAULT_IPV6_IP if self._use_ipv6 else DEFAULT_IPV4_IP, + DEFAULT_PORT ) - # Remember flood-waited requests to avoid making them again - self._flood_waited_requests = {} + self.flood_sleep_threshold = flood_sleep_threshold - # Cache ``{dc_id: (_ExportState, MTProtoSender)}`` for all borrowed senders - self._borrowed_senders = {} - self._borrow_sender_lock = asyncio.Lock() + # TODO Use AsyncClassWrapper(session) + # ChatGetter and SenderGetter can use the in-memory _entity_cache + # to avoid network access and the need for await in session files. + # + # The session files only wants the entities to persist + # them to disk, and to save additional useful information. + # TODO Session should probably return all cached + # info of entities, not just the input versions + self.session = session + self._entity_cache = EntityCache() + self.api_id = int(api_id) + self.api_hash = api_hash - self._updates_handle = None - self._last_request = time.time() - self._channel_pts = {} - self._no_updates = not receive_updates + # Current proxy implementation requires `sock_connect`, and some + # event loops lack this method. If the current loop is missing it, + # bail out early and suggest an alternative. + # + # TODO A better fix is obviously avoiding the use of `sock_connect` + # + # See https://github.com/LonamiWebs/Telethon/issues/1337 for details. + if not callable(getattr(self.loop, 'sock_connect', None)): + raise TypeError( + 'Event loop of type {} lacks `sock_connect`, which is needed to use proxies.\n\n' + 'Change the event loop in use to use proxies:\n' + '# https://github.com/LonamiWebs/Telethon/issues/1337\n' + 'import asyncio\n' + 'asyncio.set_event_loop(asyncio.SelectorEventLoop())'.format( + self.loop.__class__.__name__ + ) + ) - if sequential_updates: - self._updates_queue = asyncio.Queue() - self._dispatching_updates_queue = asyncio.Event() + if local_addr is not None: + if use_ipv6 is False and ':' in local_addr: + raise TypeError( + 'A local IPv6 address must only be used with `use_ipv6=True`.' + ) + elif use_ipv6 is True and ':' not in local_addr: + raise TypeError( + '`use_ipv6=True` must only be used with a local IPv6 address.' + ) + + self._raise_last_call_error = raise_last_call_error + + self._request_retries = request_retries + self._connection_retries = connection_retries + self._retry_delay = retry_delay or 0 + self._proxy = proxy + self._local_addr = local_addr + self._timeout = timeout + self._auto_reconnect = auto_reconnect + + assert isinstance(connection, type) + self._connection = connection + init_proxy = None if not issubclass(connection, TcpMTProxy) else \ + types.InputClientProxy(*connection.address_info(proxy)) + + # Used on connection. Capture the variables in a lambda since + # exporting clients need to create this InvokeWithLayerRequest. + system = platform.uname() + + if system.machine in ('x86_64', 'AMD64'): + default_device_model = 'PC 64bit' + elif system.machine in ('i386','i686','x86'): + default_device_model = 'PC 32bit' + else: + default_device_model = system.machine + default_system_version = re.sub(r'-.+','',system.release) + + self._init_request = functions.InitConnectionRequest( + api_id=self.api_id, + device_model=device_model or default_device_model or 'Unknown', + system_version=system_version or default_system_version or '1.0', + app_version=app_version or self.__version__, + lang_code=lang_code, + system_lang_code=system_lang_code, + lang_pack='', # "langPacks are for official apps only" + query=None, + proxy=init_proxy + ) + + self._sender = MTProtoSender( + self.session.auth_key, + loggers=self._log, + retries=self._connection_retries, + delay=self._retry_delay, + auto_reconnect=self._auto_reconnect, + connect_timeout=self._timeout, + auth_key_callback=self._auth_key_callback, + update_callback=self._handle_update, + auto_reconnect_callback=self._handle_auto_reconnect + ) + + # Remember flood-waited requests to avoid making them again + self._flood_waited_requests = {} + + # Cache ``{dc_id: (_ExportState, MTProtoSender)}`` for all borrowed senders + self._borrowed_senders = {} + self._borrow_sender_lock = asyncio.Lock() + + self._updates_handle = None + self._last_request = time.time() + self._channel_pts = {} + self._no_updates = not receive_updates + + if sequential_updates: + self._updates_queue = asyncio.Queue() + self._dispatching_updates_queue = asyncio.Event() + else: + # Use a set of pending instead of a queue so we can properly + # terminate all pending updates on disconnect. + self._updates_queue = set() + self._dispatching_updates_queue = None + + self._authorized = None # None = unknown, False = no, True = yes + + # Update state (for catching up after a disconnection) + # TODO Get state from channels too + self._state_cache = StateCache( + self.session.get_update_state(0), self._log) + + # Some further state for subclasses + self._event_builders = [] + + # {chat_id: {Conversation}} + self._conversations = collections.defaultdict(set) + + # Hack to workaround the fact Telegram may send album updates as + # different Updates when being sent from a different data center. + # {grouped_id: AlbumHack} + # + # FIXME: We don't bother cleaning this up because it's not really + # worth it, albums are pretty rare and this only holds them + # for a second at most. + self._albums = {} + + # Default parse mode + self._parse_mode = markdown + + # Some fields to easy signing in. Let {phone: hash} be + # a dictionary because the user may change their mind. + self._phone_code_hash = {} + self._phone = None + self._tos = None + + # Sometimes we need to know who we are, cache the self peer + self._self_input_peer = None + self._bot = None + + # A place to store if channels are a megagroup or not (see `edit_admin`) + self._megagroup_cache = {} + + +def get_loop(self: 'TelegramClient') -> asyncio.AbstractEventLoop: + return asyncio.get_event_loop() + +def get_disconnected(self: 'TelegramClient') -> asyncio.Future: + return self._sender.disconnected + +def get_flood_sleep_threshold(self): + return self._flood_sleep_threshold + +def set_flood_sleep_threshold(self, value): + # None -> 0, negative values don't really matter + self._flood_sleep_threshold = min(value or 0, 24 * 60 * 60) + + +async def connect(self: 'TelegramClient') -> None: + if not await self._sender.connect(self._connection( + self.session.server_address, + self.session.port, + self.session.dc_id, + loggers=self._log, + proxy=self._proxy, + local_addr=self._local_addr + )): + # We don't want to init or modify anything if we were already connected + return + + self.session.auth_key = self._sender.auth_key + self.session.save() + + self._init_request.query = functions.help.GetConfigRequest() + + await self._sender.send(functions.InvokeWithLayerRequest( + LAYER, self._init_request + )) + + self._updates_handle = self.loop.create_task(self._update_loop()) + +def is_connected(self: 'TelegramClient') -> bool: + sender = getattr(self, '_sender', None) + return sender and sender.is_connected() + +def disconnect(self: 'TelegramClient'): + if self.loop.is_running(): + return self._disconnect_coro() + else: + try: + self.loop.run_until_complete(self._disconnect_coro()) + except RuntimeError: + # Python 3.5.x complains when called from + # `__aexit__` and there were pending updates with: + # "Event loop stopped before Future completed." + # + # However, it doesn't really make a lot of sense. + pass + +def set_proxy(self: 'TelegramClient', proxy: typing.Union[tuple, dict]): + init_proxy = None if not issubclass(self._connection, TcpMTProxy) else \ + types.InputClientProxy(*self._connection.address_info(proxy)) + + self._init_request.proxy = init_proxy + self._proxy = proxy + + # While `await client.connect()` passes new proxy on each new call, + # auto-reconnect attempts use already set up `_connection` inside + # the `_sender`, so the only way to change proxy between those + # is to directly inject parameters. + + connection = getattr(self._sender, "_connection", None) + if connection: + if isinstance(connection, TcpMTProxy): + connection._ip = proxy[0] + connection._port = proxy[1] else: - # Use a set of pending instead of a queue so we can properly - # terminate all pending updates on disconnect. - self._updates_queue = set() - self._dispatching_updates_queue = None + connection._proxy = proxy - self._authorized = None # None = unknown, False = no, True = yes +async def _disconnect_coro(self: 'TelegramClient'): + await self._disconnect() - # Update state (for catching up after a disconnection) - # TODO Get state from channels too - self._state_cache = StateCache( - self.session.get_update_state(0), self._log) + # Also clean-up all exported senders because we're done with them + async with self._borrow_sender_lock: + for state, sender in self._borrowed_senders.values(): + # Note that we're not checking for `state.should_disconnect()`. + # If the user wants to disconnect the client, ALL connections + # to Telegram (including exported senders) should be closed. + # + # Disconnect should never raise, so there's no try/except. + await sender.disconnect() + # Can't use `mark_disconnected` because it may be borrowed. + state._connected = False - # Some further state for subclasses - self._event_builders = [] + # If any was borrowed + self._borrowed_senders.clear() - # {chat_id: {Conversation}} - self._conversations = collections.defaultdict(set) + # trio's nurseries would handle this for us, but this is asyncio. + # All tasks spawned in the background should properly be terminated. + if self._dispatching_updates_queue is None and self._updates_queue: + for task in self._updates_queue: + task.cancel() - # Hack to workaround the fact Telegram may send album updates as - # different Updates when being sent from a different data center. - # {grouped_id: AlbumHack} - # - # FIXME: We don't bother cleaning this up because it's not really - # worth it, albums are pretty rare and this only holds them - # for a second at most. - self._albums = {} + await asyncio.wait(self._updates_queue) + self._updates_queue.clear() - # Default parse mode - self._parse_mode = markdown - - # Some fields to easy signing in. Let {phone: hash} be - # a dictionary because the user may change their mind. - self._phone_code_hash = {} - self._phone = None - self._tos = None - - # Sometimes we need to know who we are, cache the self peer - self._self_input_peer = None - self._bot = None - - # A place to store if channels are a megagroup or not (see `edit_admin`) - self._megagroup_cache = {} - - # endregion - - # region Properties - - @property - def loop(self: 'TelegramClient') -> asyncio.AbstractEventLoop: - """ - Property with the ``asyncio`` event loop used by this client. - - Example - .. code-block:: python - - # Download media in the background - task = client.loop.create_task(message.download_media()) - - # Do some work - ... - - # Join the task (wait for it to complete) - await task - """ - return asyncio.get_event_loop() - - @property - def disconnected(self: 'TelegramClient') -> asyncio.Future: - """ - Property with a ``Future`` that resolves upon disconnection. - - Example - .. code-block:: python - - # Wait for a disconnection to occur - try: - await client.disconnected - except OSError: - print('Error on disconnect') - """ - return self._sender.disconnected - - @property - def flood_sleep_threshold(self): - return self._flood_sleep_threshold - - @flood_sleep_threshold.setter - def flood_sleep_threshold(self, value): - # None -> 0, negative values don't really matter - self._flood_sleep_threshold = min(value or 0, 24 * 60 * 60) - - # endregion - - # region Connecting - - async def connect(self: 'TelegramClient') -> None: - """ - Connects to Telegram. - - .. note:: - - Connect means connect and nothing else, and only one low-level - request is made to notify Telegram about which layer we will be - using. - - Before Telegram sends you updates, you need to make a high-level - request, like `client.get_me() `, - as described in https://core.telegram.org/api/updates. - - Example - .. code-block:: python - - try: - await client.connect() - except OSError: - print('Failed to connect') - """ - if not await self._sender.connect(self._connection( - self.session.server_address, - self.session.port, - self.session.dc_id, - loggers=self._log, - proxy=self._proxy, - local_addr=self._local_addr - )): - # We don't want to init or modify anything if we were already connected - return - - self.session.auth_key = self._sender.auth_key - self.session.save() - - self._init_request.query = functions.help.GetConfigRequest() - - await self._sender.send(functions.InvokeWithLayerRequest( - LAYER, self._init_request + pts, date = self._state_cache[None] + if pts and date: + self.session.set_update_state(0, types.updates.State( + pts=pts, + qts=0, + date=date, + seq=0, + unread_count=0 )) - self._updates_handle = self.loop.create_task(self._update_loop()) + self.session.close() - def is_connected(self: 'TelegramClient') -> bool: - """ - Returns `True` if the user has connected. +async def _disconnect(self: 'TelegramClient'): + """ + Disconnect only, without closing the session. Used in reconnections + to different data centers, where we don't want to close the session + file; user disconnects however should close it since it means that + their job with the client is complete and we should clean it up all. + """ + await self._sender.disconnect() + await helpers._cancel(self._log[__name__], + updates_handle=self._updates_handle) - This method is **not** asynchronous (don't use ``await`` on it). +async def _switch_dc(self: 'TelegramClient', new_dc): + """ + Permanently switches the current connection to the new data center. + """ + self._log[__name__].info('Reconnecting to new data center %s', new_dc) + dc = await self._get_dc(new_dc) - Example - .. code-block:: python + self.session.set_dc(dc.id, dc.ip_address, dc.port) + # auth_key's are associated with a server, which has now changed + # so it's not valid anymore. Set to None to force recreating it. + self._sender.auth_key.key = None + self.session.auth_key = None + self.session.save() + await self._disconnect() + return await self.connect() - while client.is_connected(): - await asyncio.sleep(1) - """ - sender = getattr(self, '_sender', None) - return sender and sender.is_connected() +def _auth_key_callback(self: 'TelegramClient', auth_key): + """ + Callback from the sender whenever it needed to generate a + new authorization key. This means we are not authorized. + """ + self.session.auth_key = auth_key + self.session.save() - def disconnect(self: 'TelegramClient'): - """ - Disconnects from Telegram. - If the event loop is already running, this method returns a - coroutine that you should await on your own code; otherwise - the loop is ran until said coroutine completes. +async def _get_dc(self: 'TelegramClient', dc_id, cdn=False): + """Gets the Data Center (DC) associated to 'dc_id'""" + cls = self.__class__ + if not cls._config: + cls._config = await self(functions.help.GetConfigRequest()) - Example - .. code-block:: python + if cdn and not self._cdn_config: + cls._cdn_config = await self(functions.help.GetCdnConfigRequest()) + for pk in cls._cdn_config.public_keys: + rsa.add_key(pk.public_key) - # You don't need to use this if you used "with client" - await client.disconnect() - """ - if self.loop.is_running(): - return self._disconnect_coro() - else: - try: - self.loop.run_until_complete(self._disconnect_coro()) - except RuntimeError: - # Python 3.5.x complains when called from - # `__aexit__` and there were pending updates with: - # "Event loop stopped before Future completed." - # - # However, it doesn't really make a lot of sense. - pass + try: + return next( + dc for dc in cls._config.dc_options + if dc.id == dc_id + and bool(dc.ipv6) == self._use_ipv6 and bool(dc.cdn) == cdn + ) + except StopIteration: + self._log[__name__].warning( + 'Failed to get DC %s (cdn = %s) with use_ipv6 = %s; retrying ignoring IPv6 check', + dc_id, cdn, self._use_ipv6 + ) + return next( + dc for dc in cls._config.dc_options + if dc.id == dc_id and bool(dc.cdn) == cdn + ) - def set_proxy(self: 'TelegramClient', proxy: typing.Union[tuple, dict]): - """ - Changes the proxy which will be used on next (re)connection. +async def _create_exported_sender(self: 'TelegramClient', dc_id): + """ + Creates a new exported `MTProtoSender` for the given `dc_id` and + returns it. This method should be used by `_borrow_exported_sender`. + """ + # Thanks badoualy/kotlogram on /telegram/api/DefaultTelegramClient.kt + # for clearly showing how to export the authorization + dc = await self._get_dc(dc_id) + # Can't reuse self._sender._connection as it has its own seqno. + # + # If one were to do that, Telegram would reset the connection + # with no further clues. + sender = MTProtoSender(None, loggers=self._log) + await sender.connect(self._connection( + dc.ip_address, + dc.port, + dc.id, + loggers=self._log, + proxy=self._proxy, + local_addr=self._local_addr + )) + self._log[__name__].info('Exporting auth for new borrowed sender in %s', dc) + auth = await self(functions.auth.ExportAuthorizationRequest(dc_id)) + self._init_request.query = functions.auth.ImportAuthorizationRequest(id=auth.id, bytes=auth.bytes) + req = functions.InvokeWithLayerRequest(LAYER, self._init_request) + await sender.send(req) + return sender - Method has no immediate effects if the client is currently connected. +async def _borrow_exported_sender(self: 'TelegramClient', dc_id): + """ + Borrows a connected `MTProtoSender` for the given `dc_id`. + If it's not cached, creates a new one if it doesn't exist yet, + and imports a freshly exported authorization key for it to be usable. - The new proxy will take it's effect on the next reconnection attempt: - - on a call `await client.connect()` (after complete disconnect) - - on auto-reconnect attempt (e.g, after previous connection was lost) - """ - init_proxy = None if not issubclass(self._connection, TcpMTProxy) else \ - types.InputClientProxy(*self._connection.address_info(proxy)) + Once its job is over it should be `_return_exported_sender`. + """ + async with self._borrow_sender_lock: + self._log[__name__].debug('Borrowing sender for dc_id %d', dc_id) + state, sender = self._borrowed_senders.get(dc_id, (None, None)) - self._init_request.proxy = init_proxy - self._proxy = proxy + if state is None: + state = _ExportState() + sender = await self._create_exported_sender(dc_id) + sender.dc_id = dc_id + self._borrowed_senders[dc_id] = (state, sender) - # While `await client.connect()` passes new proxy on each new call, - # auto-reconnect attempts use already set up `_connection` inside - # the `_sender`, so the only way to change proxy between those - # is to directly inject parameters. - - connection = getattr(self._sender, "_connection", None) - if connection: - if isinstance(connection, TcpMTProxy): - connection._ip = proxy[0] - connection._port = proxy[1] - else: - connection._proxy = proxy - - async def _disconnect_coro(self: 'TelegramClient'): - await self._disconnect() - - # Also clean-up all exported senders because we're done with them - async with self._borrow_sender_lock: - for state, sender in self._borrowed_senders.values(): - # Note that we're not checking for `state.should_disconnect()`. - # If the user wants to disconnect the client, ALL connections - # to Telegram (including exported senders) should be closed. - # - # Disconnect should never raise, so there's no try/except. - await sender.disconnect() - # Can't use `mark_disconnected` because it may be borrowed. - state._connected = False - - # If any was borrowed - self._borrowed_senders.clear() - - # trio's nurseries would handle this for us, but this is asyncio. - # All tasks spawned in the background should properly be terminated. - if self._dispatching_updates_queue is None and self._updates_queue: - for task in self._updates_queue: - task.cancel() - - await asyncio.wait(self._updates_queue) - self._updates_queue.clear() - - pts, date = self._state_cache[None] - if pts and date: - self.session.set_update_state(0, types.updates.State( - pts=pts, - qts=0, - date=date, - seq=0, - unread_count=0 + elif state.need_connect(): + dc = await self._get_dc(dc_id) + await sender.connect(self._connection( + dc.ip_address, + dc.port, + dc.id, + loggers=self._log, + proxy=self._proxy, + local_addr=self._local_addr )) - self.session.close() - - async def _disconnect(self: 'TelegramClient'): - """ - Disconnect only, without closing the session. Used in reconnections - to different data centers, where we don't want to close the session - file; user disconnects however should close it since it means that - their job with the client is complete and we should clean it up all. - """ - await self._sender.disconnect() - await helpers._cancel(self._log[__name__], - updates_handle=self._updates_handle) - - async def _switch_dc(self: 'TelegramClient', new_dc): - """ - Permanently switches the current connection to the new data center. - """ - self._log[__name__].info('Reconnecting to new data center %s', new_dc) - dc = await self._get_dc(new_dc) - - self.session.set_dc(dc.id, dc.ip_address, dc.port) - # auth_key's are associated with a server, which has now changed - # so it's not valid anymore. Set to None to force recreating it. - self._sender.auth_key.key = None - self.session.auth_key = None - self.session.save() - await self._disconnect() - return await self.connect() - - def _auth_key_callback(self: 'TelegramClient', auth_key): - """ - Callback from the sender whenever it needed to generate a - new authorization key. This means we are not authorized. - """ - self.session.auth_key = auth_key - self.session.save() - - # endregion - - # region Working with different connections/Data Centers - - async def _get_dc(self: 'TelegramClient', dc_id, cdn=False): - """Gets the Data Center (DC) associated to 'dc_id'""" - cls = self.__class__ - if not cls._config: - cls._config = await self(functions.help.GetConfigRequest()) - - if cdn and not self._cdn_config: - cls._cdn_config = await self(functions.help.GetCdnConfigRequest()) - for pk in cls._cdn_config.public_keys: - rsa.add_key(pk.public_key) - - try: - return next( - dc for dc in cls._config.dc_options - if dc.id == dc_id - and bool(dc.ipv6) == self._use_ipv6 and bool(dc.cdn) == cdn - ) - except StopIteration: - self._log[__name__].warning( - 'Failed to get DC %s (cdn = %s) with use_ipv6 = %s; retrying ignoring IPv6 check', - dc_id, cdn, self._use_ipv6 - ) - return next( - dc for dc in cls._config.dc_options - if dc.id == dc_id and bool(dc.cdn) == cdn - ) - - async def _create_exported_sender(self: 'TelegramClient', dc_id): - """ - Creates a new exported `MTProtoSender` for the given `dc_id` and - returns it. This method should be used by `_borrow_exported_sender`. - """ - # Thanks badoualy/kotlogram on /telegram/api/DefaultTelegramClient.kt - # for clearly showing how to export the authorization - dc = await self._get_dc(dc_id) - # Can't reuse self._sender._connection as it has its own seqno. - # - # If one were to do that, Telegram would reset the connection - # with no further clues. - sender = MTProtoSender(None, loggers=self._log) - await sender.connect(self._connection( - dc.ip_address, - dc.port, - dc.id, - loggers=self._log, - proxy=self._proxy, - local_addr=self._local_addr - )) - self._log[__name__].info('Exporting auth for new borrowed sender in %s', dc) - auth = await self(functions.auth.ExportAuthorizationRequest(dc_id)) - self._init_request.query = functions.auth.ImportAuthorizationRequest(id=auth.id, bytes=auth.bytes) - req = functions.InvokeWithLayerRequest(LAYER, self._init_request) - await sender.send(req) + state.add_borrow() return sender - async def _borrow_exported_sender(self: 'TelegramClient', dc_id): - """ - Borrows a connected `MTProtoSender` for the given `dc_id`. - If it's not cached, creates a new one if it doesn't exist yet, - and imports a freshly exported authorization key for it to be usable. +async def _return_exported_sender(self: 'TelegramClient', sender): + """ + Returns a borrowed exported sender. If all borrows have + been returned, the sender is cleanly disconnected. + """ + async with self._borrow_sender_lock: + self._log[__name__].debug('Returning borrowed sender for dc_id %d', sender.dc_id) + state, _ = self._borrowed_senders[sender.dc_id] + state.add_return() - Once its job is over it should be `_return_exported_sender`. - """ - async with self._borrow_sender_lock: - self._log[__name__].debug('Borrowing sender for dc_id %d', dc_id) - state, sender = self._borrowed_senders.get(dc_id, (None, None)) +async def _clean_exported_senders(self: 'TelegramClient'): + """ + Cleans-up all unused exported senders by disconnecting them. + """ + async with self._borrow_sender_lock: + for dc_id, (state, sender) in self._borrowed_senders.items(): + if state.should_disconnect(): + self._log[__name__].info( + 'Disconnecting borrowed sender for DC %d', dc_id) - if state is None: - state = _ExportState() - sender = await self._create_exported_sender(dc_id) - sender.dc_id = dc_id - self._borrowed_senders[dc_id] = (state, sender) + # Disconnect should never raise + await sender.disconnect() + state.mark_disconnected() - elif state.need_connect(): - dc = await self._get_dc(dc_id) - await sender.connect(self._connection( - dc.ip_address, - dc.port, - dc.id, - loggers=self._log, - proxy=self._proxy, - local_addr=self._local_addr - )) +async def _get_cdn_client(self: 'TelegramClient', cdn_redirect): + """Similar to ._borrow_exported_client, but for CDNs""" + # TODO Implement + raise NotImplementedError + session = self._exported_sessions.get(cdn_redirect.dc_id) + if not session: + dc = await self._get_dc(cdn_redirect.dc_id, cdn=True) + session = self.session.clone() + await session.set_dc(dc.id, dc.ip_address, dc.port) + self._exported_sessions[cdn_redirect.dc_id] = session - state.add_borrow() - return sender + self._log[__name__].info('Creating new CDN client') + client = TelegramBaseClient( + session, self.api_id, self.api_hash, + proxy=self._sender.connection.conn.proxy, + timeout=self._sender.connection.get_timeout() + ) - async def _return_exported_sender(self: 'TelegramClient', sender): - """ - Returns a borrowed exported sender. If all borrows have - been returned, the sender is cleanly disconnected. - """ - async with self._borrow_sender_lock: - self._log[__name__].debug('Returning borrowed sender for dc_id %d', sender.dc_id) - state, _ = self._borrowed_senders[sender.dc_id] - state.add_return() + # This will make use of the new RSA keys for this specific CDN. + # + # We won't be calling GetConfigRequest because it's only called + # when needed by ._get_dc, and also it's static so it's likely + # set already. Avoid invoking non-CDN methods by not syncing updates. + client.connect(_sync_updates=False) + return client - async def _clean_exported_senders(self: 'TelegramClient'): - """ - Cleans-up all unused exported senders by disconnecting them. - """ - async with self._borrow_sender_lock: - for dc_id, (state, sender) in self._borrowed_senders.items(): - if state.should_disconnect(): - self._log[__name__].info( - 'Disconnecting borrowed sender for DC %d', dc_id) - # Disconnect should never raise - await sender.disconnect() - state.mark_disconnected() +@abc.abstractmethod +def __call__(self: 'TelegramClient', request, ordered=False): + raise NotImplementedError - async def _get_cdn_client(self: 'TelegramClient', cdn_redirect): - """Similar to ._borrow_exported_client, but for CDNs""" - # TODO Implement - raise NotImplementedError - session = self._exported_sessions.get(cdn_redirect.dc_id) - if not session: - dc = await self._get_dc(cdn_redirect.dc_id, cdn=True) - session = self.session.clone() - await session.set_dc(dc.id, dc.ip_address, dc.port) - self._exported_sessions[cdn_redirect.dc_id] = session +@abc.abstractmethod +def _handle_update(self: 'TelegramClient', update): + raise NotImplementedError - self._log[__name__].info('Creating new CDN client') - client = TelegramBaseClient( - session, self.api_id, self.api_hash, - proxy=self._sender.connection.conn.proxy, - timeout=self._sender.connection.get_timeout() - ) +@abc.abstractmethod +def _update_loop(self: 'TelegramClient'): + raise NotImplementedError - # This will make use of the new RSA keys for this specific CDN. - # - # We won't be calling GetConfigRequest because it's only called - # when needed by ._get_dc, and also it's static so it's likely - # set already. Avoid invoking non-CDN methods by not syncing updates. - client.connect(_sync_updates=False) - return client - - # endregion - - # region Invoking Telegram requests - - @abc.abstractmethod - def __call__(self: 'TelegramClient', request, ordered=False): - """ - Invokes (sends) one or more MTProtoRequests and returns (receives) - their result. - - Args: - request (`TLObject` | `list`): - The request or requests to be invoked. - - ordered (`bool`, optional): - Whether the requests (if more than one was given) should be - executed sequentially on the server. They run in arbitrary - order by default. - - flood_sleep_threshold (`int` | `None`, optional): - The flood sleep threshold to use for this request. This overrides - the default value stored in - `client.flood_sleep_threshold ` - - Returns: - The result of the request (often a `TLObject`) or a list of - results if more than one request was given. - """ - raise NotImplementedError - - @abc.abstractmethod - def _handle_update(self: 'TelegramClient', update): - raise NotImplementedError - - @abc.abstractmethod - def _update_loop(self: 'TelegramClient'): - raise NotImplementedError - - @abc.abstractmethod - async def _handle_auto_reconnect(self: 'TelegramClient'): - raise NotImplementedError - - # endregion +@abc.abstractmethod +async def _handle_auto_reconnect(self: 'TelegramClient'): + raise NotImplementedError diff --git a/telethon/client/telegramclient.py b/telethon/client/telegramclient.py index 144a6b2f..f1fbaa82 100644 --- a/telethon/client/telegramclient.py +++ b/telethon/client/telegramclient.py @@ -1,13 +1,3845 @@ +import functools +import inspect +import typing + from . import ( - AccountMethods, AuthMethods, DownloadMethods, DialogMethods, ChatMethods, - BotMethods, MessageMethods, UploadMethods, ButtonMethods, UpdateMethods, - MessageParseMethods, UserMethods, TelegramBaseClient + account, auth, bots, buttons, chats, dialogs, downloads, messageparse, messages, + telegrambaseclient, updates, uploads, users ) +from .. import helpers -class TelegramClient( - AccountMethods, AuthMethods, DownloadMethods, DialogMethods, ChatMethods, - BotMethods, MessageMethods, UploadMethods, ButtonMethods, UpdateMethods, - MessageParseMethods, UserMethods, TelegramBaseClient -): - pass +class TelegramClient: + """ + Arguments + session (`str` | `telethon.sessions.abstract.Session`, `None`): + The file name of the session file to be used if a string is + given (it may be a full path), or the Session instance to be + used otherwise. If it's `None`, the session will not be saved, + and you should call :meth:`.log_out()` when you're done. + + Note that if you pass a string it will be a file in the current + working directory, although you can also pass absolute paths. + + The session file contains enough information for you to login + without re-sending the code, so if you have to enter the code + more than once, maybe you're changing the working directory, + renaming or removing the file, or using random names. + + api_id (`int` | `str`): + The API ID you obtained from https://my.telegram.org. + + api_hash (`str`): + The API hash you obtained from https://my.telegram.org. + + connection (`telethon.network.connection.common.Connection`, optional): + The connection instance to be used when creating a new connection + to the servers. It **must** be a type. + + Defaults to `telethon.network.connection.tcpfull.ConnectionTcpFull`. + + use_ipv6 (`bool`, optional): + Whether to connect to the servers through IPv6 or not. + By default this is `False` as IPv6 support is not + too widespread yet. + + proxy (`tuple` | `list` | `dict`, optional): + An iterable consisting of the proxy info. If `connection` is + one of `MTProxy`, then it should contain MTProxy credentials: + ``('hostname', port, 'secret')``. Otherwise, it's meant to store + function parameters for PySocks, like ``(type, 'hostname', port)``. + See https://github.com/Anorov/PySocks#usage-1 for more. + + local_addr (`str` | `tuple`, optional): + Local host address (and port, optionally) used to bind the socket to locally. + You only need to use this if you have multiple network cards and + want to use a specific one. + + timeout (`int` | `float`, optional): + The timeout in seconds to be used when connecting. + This is **not** the timeout to be used when ``await``'ing for + invoked requests, and you should use ``asyncio.wait`` or + ``asyncio.wait_for`` for that. + + request_retries (`int` | `None`, optional): + How many times a request should be retried. Request are retried + when Telegram is having internal issues (due to either + ``errors.ServerError`` or ``errors.RpcCallFailError``), + when there is a ``errors.FloodWaitError`` less than + `flood_sleep_threshold`, or when there's a migrate error. + + May take a negative or `None` value for infinite retries, but + this is not recommended, since some requests can always trigger + a call fail (such as searching for messages). + + connection_retries (`int` | `None`, optional): + How many times the reconnection should retry, either on the + initial connection or when Telegram disconnects us. May be + set to a negative or `None` value for infinite retries, but + this is not recommended, since the program can get stuck in an + infinite loop. + + retry_delay (`int` | `float`, optional): + The delay in seconds to sleep between automatic reconnections. + + auto_reconnect (`bool`, optional): + Whether reconnection should be retried `connection_retries` + times automatically if Telegram disconnects us or not. + + sequential_updates (`bool`, optional): + By default every incoming update will create a new task, so + you can handle several updates in parallel. Some scripts need + the order in which updates are processed to be sequential, and + this setting allows them to do so. + + If set to `True`, incoming updates will be put in a queue + and processed sequentially. This means your event handlers + should *not* perform long-running operations since new + updates are put inside of an unbounded queue. + + flood_sleep_threshold (`int` | `float`, optional): + The threshold below which the library should automatically + sleep on flood wait and slow mode wait errors (inclusive). For instance, if a + ``FloodWaitError`` for 17s occurs and `flood_sleep_threshold` + is 20s, the library will ``sleep`` automatically. If the error + was for 21s, it would ``raise FloodWaitError`` instead. Values + larger than a day (like ``float('inf')``) will be changed to a day. + + raise_last_call_error (`bool`, optional): + When API calls fail in a way that causes Telethon to retry + automatically, should the RPC error of the last attempt be raised + instead of a generic ValueError. This is mostly useful for + detecting when Telegram has internal issues. + + device_model (`str`, optional): + "Device model" to be sent when creating the initial connection. + Defaults to 'PC (n)bit' derived from ``platform.uname().machine``, or its direct value if unknown. + + system_version (`str`, optional): + "System version" to be sent when creating the initial connection. + Defaults to ``platform.uname().release`` stripped of everything ahead of -. + + app_version (`str`, optional): + "App version" to be sent when creating the initial connection. + Defaults to `telethon.version.__version__`. + + lang_code (`str`, optional): + "Language code" to be sent when creating the initial connection. + Defaults to ``'en'``. + + system_lang_code (`str`, optional): + "System lang code" to be sent when creating the initial connection. + Defaults to `lang_code`. + + loop (`asyncio.AbstractEventLoop`, optional): + Asyncio event loop to use. Defaults to `asyncio.get_event_loop()`. + This argument is ignored. + + base_logger (`str` | `logging.Logger`, optional): + Base logger name or instance to use. + If a `str` is given, it'll be passed to `logging.getLogger()`. If a + `logging.Logger` is given, it'll be used directly. If something + else or nothing is given, the default logger will be used. + + receive_updates (`bool`, optional): + Whether the client will receive updates or not. By default, updates + will be received from Telegram as they occur. + + Turning this off means that Telegram will not send updates at all + so event handlers, conversations, and QR login will not work. + However, certain scripts don't need updates, so this will reduce + the amount of bandwidth used. + """ + + # region Account + + 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. + + 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: + + 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 + `. + + 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 + `. + + 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') + """ + return account.takeout(**locals()) + + 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) + """ + return await account.end_takeout(**locals()) + + # endregion Account + + # region Auth + + def start( + self: 'TelegramClient', + phone: typing.Callable[[], str] = lambda: input('Please enter your phone (or bot token): '), + password: typing.Callable[[], str] = lambda: getpass.getpass('Please enter your password: '), + *, + bot_token: str = None, + force_sms: bool = False, + code_callback: typing.Callable[[], typing.Union[str, int]] = None, + first_name: str = 'New User', + last_name: str = '', + max_attempts: int = 3) -> 'TelegramClient': + """ + Starts the client (connects and logs in if necessary). + + By default, this method will be interactive (asking for + user input if needed), and will handle 2FA if enabled too. + + If the phone doesn't belong to an existing account (and will hence + `sign_up` for a new one), **you are agreeing to Telegram's + Terms of Service. This is required and your account + will be banned otherwise.** See https://telegram.org/tos + and https://core.telegram.org/api/terms. + + If the event loop is already running, this method returns a + coroutine that you should await on your own code; otherwise + the loop is ran until said coroutine completes. + + Arguments + phone (`str` | `int` | `callable`): + The phone (or callable without arguments to get it) + to which the code will be sent. If a bot-token-like + string is given, it will be used as such instead. + The argument may be a coroutine. + + password (`str`, `callable`, optional): + The password for 2 Factor Authentication (2FA). + This is only required if it is enabled in your account. + The argument may be a coroutine. + + bot_token (`str`): + Bot Token obtained by `@BotFather `_ + to log in as a bot. Cannot be specified with ``phone`` (only + one of either allowed). + + force_sms (`bool`, optional): + Whether to force sending the code request as SMS. + This only makes sense when signing in with a `phone`. + + code_callback (`callable`, optional): + A callable that will be used to retrieve the Telegram + login code. Defaults to `input()`. + The argument may be a coroutine. + + first_name (`str`, optional): + The first name to be used if signing up. This has no + effect if the account already exists and you sign in. + + last_name (`str`, optional): + Similar to the first name, but for the last. Optional. + + max_attempts (`int`, optional): + How many times the code/password callback should be + retried or switching between signing in and signing up. + + Returns + This `TelegramClient`, so initialization + can be chained with ``.start()``. + + Example + .. code-block:: python + + client = TelegramClient('anon', api_id, api_hash) + + # Starting as a bot account + await client.start(bot_token=bot_token) + + # Starting as a user account + await client.start(phone) + # Please enter the code you received: 12345 + # Please enter your password: ******* + # (You are now logged in) + + # Starting using a context manager (this calls start()): + with client: + pass + """ + return auth.start(**locals()) + + async def sign_in( + self: 'TelegramClient', + phone: str = None, + code: typing.Union[str, int] = None, + *, + password: str = None, + bot_token: str = None, + phone_code_hash: str = None) -> 'typing.Union[types.User, types.auth.SentCode]': + """ + Logs in to Telegram to an existing user or bot account. + + You should only use this if you are not authorized yet. + + This method will send the code if it's not provided. + + .. note:: + + In most cases, you should simply use `start()` and not this method. + + Arguments + phone (`str` | `int`): + The phone to send the code to if no code was provided, + or to override the phone that was previously used with + these requests. + + code (`str` | `int`): + The code that Telegram sent. Note that if you have sent this + code through the application itself it will immediately + expire. If you want to send the code, obfuscate it somehow. + If you're not doing any of this you can ignore this note. + + password (`str`): + 2FA password, should be used if a previous call raised + ``SessionPasswordNeededError``. + + bot_token (`str`): + Used to sign in as a bot. Not all requests will be available. + This should be the hash the `@BotFather `_ + gave you. + + phone_code_hash (`str`, optional): + The hash returned by `send_code_request`. This can be left as + `None` to use the last hash known for the phone to be used. + + Returns + The signed in user, or the information about + :meth:`send_code_request`. + + Example + .. code-block:: python + + phone = '+34 123 123 123' + await client.sign_in(phone) # send code + + code = input('enter code: ') + await client.sign_in(phone, code) + """ + return auth.sign_in(**locals()) + + async def sign_up( + self: 'TelegramClient', + code: typing.Union[str, int], + first_name: str, + last_name: str = '', + *, + phone: str = None, + phone_code_hash: str = None) -> 'types.User': + """ + Signs up to Telegram as a new user account. + + Use this if you don't have an account yet. + + You must call `send_code_request` first. + + **By using this method you're agreeing to Telegram's + Terms of Service. This is required and your account + will be banned otherwise.** See https://telegram.org/tos + and https://core.telegram.org/api/terms. + + Arguments + code (`str` | `int`): + The code sent by Telegram + + first_name (`str`): + The first name to be used by the new account. + + last_name (`str`, optional) + Optional last name. + + phone (`str` | `int`, optional): + The phone to sign up. This will be the last phone used by + default (you normally don't need to set this). + + phone_code_hash (`str`, optional): + The hash returned by `send_code_request`. This can be left as + `None` to use the last hash known for the phone to be used. + + Returns + The new created :tl:`User`. + + Example + .. code-block:: python + + phone = '+34 123 123 123' + await client.send_code_request(phone) + + code = input('enter code: ') + await client.sign_up(code, first_name='Anna', last_name='Banana') + """ + return auth.sign_up(**locals()) + + async def send_code_request( + self: 'TelegramClient', + phone: str, + *, + force_sms: bool = False) -> 'types.auth.SentCode': + """ + Sends the Telegram code needed to login to the given phone number. + + Arguments + phone (`str` | `int`): + The phone to which the code will be sent. + + force_sms (`bool`, optional): + Whether to force sending as SMS. + + Returns + An instance of :tl:`SentCode`. + + Example + .. code-block:: python + + phone = '+34 123 123 123' + sent = await client.send_code_request(phone) + print(sent) + """ + return auth.send_code_request(**locals()) + + async def qr_login(self: 'TelegramClient', ignored_ids: typing.List[int] = None) -> custom.QRLogin: + """ + Initiates the QR login procedure. + + Note that you must be connected before invoking this, as with any + other request. + + It is up to the caller to decide how to present the code to the user, + whether it's the URL, using the token bytes directly, or generating + a QR code and displaying it by other means. + + See the documentation for `QRLogin` to see how to proceed after this. + + Arguments + ignored_ids (List[`int`]): + List of already logged-in user IDs, to prevent logging in + twice with the same user. + + Returns + An instance of `QRLogin`. + + Example + .. code-block:: python + + def display_url_as_qr(url): + pass # do whatever to show url as a qr to the user + + qr_login = await client.qr_login() + display_url_as_qr(qr_login.url) + + # Important! You need to wait for the login to complete! + await qr_login.wait() + """ + return auth.qr_login(**locals()) + + async def log_out(self: 'TelegramClient') -> bool: + """ + Logs out Telegram and deletes the current ``*.session`` file. + + Returns + `True` if the operation was successful. + + Example + .. code-block:: python + + # Note: you will need to login again! + await client.log_out() + """ + return auth.log_out(**locals()) + + async def edit_2fa( + self: 'TelegramClient', + current_password: str = None, + new_password: str = None, + *, + hint: str = '', + email: str = None, + email_code_callback: typing.Callable[[int], str] = None) -> bool: + """ + Changes the 2FA settings of the logged in user. + + Review carefully the parameter explanations before using this method. + + Note that this method may be *incredibly* slow depending on the + prime numbers that must be used during the process to make sure + that everything is safe. + + Has no effect if both current and new password are omitted. + + Arguments + current_password (`str`, optional): + The current password, to authorize changing to ``new_password``. + Must be set if changing existing 2FA settings. + Must **not** be set if 2FA is currently disabled. + Passing this by itself will remove 2FA (if correct). + + new_password (`str`, optional): + The password to set as 2FA. + If 2FA was already enabled, ``current_password`` **must** be set. + Leaving this blank or `None` will remove the password. + + hint (`str`, optional): + Hint to be displayed by Telegram when it asks for 2FA. + Leaving unspecified is highly discouraged. + Has no effect if ``new_password`` is not set. + + email (`str`, optional): + Recovery and verification email. If present, you must also + set `email_code_callback`, else it raises ``ValueError``. + + email_code_callback (`callable`, optional): + If an email is provided, a callback that returns the code sent + to it must also be set. This callback may be asynchronous. + It should return a string with the code. The length of the + code will be passed to the callback as an input parameter. + + If the callback returns an invalid code, it will raise + ``CodeInvalidError``. + + Returns + `True` if successful, `False` otherwise. + + Example + .. code-block:: python + + # Setting a password for your account which didn't have + await client.edit_2fa(new_password='I_<3_Telethon') + + # Removing the password + await client.edit_2fa(current_password='I_<3_Telethon') + """ + return auth.edit_2fa(**locals()) + + async def __aenter__(self): + return await self.start() + + async def __aexit__(self, *args): + await self.disconnect() + + __enter__ = helpers._sync_enter + __exit__ = helpers._sync_exit + + # endregion Auth + + # region Bots + + 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``). + + Arguments + bot (`entity`): + The bot entity to which the inline query should be made. + + 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 + `. + + 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') + """ + return bots.inline_query(**locals()) + + # endregion Bots + + # region Buttons + + @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. + + Does nothing if either no buttons are provided or the provided + argument is already a reply markup. + + You should consider using this method if you are going to reuse + the markup very often. Otherwise, it is not necessary. + + This method is **not** asynchronous (don't use ``await`` on it). + + Arguments + buttons (`hints.MarkupLike`): + The button, list of buttons, array of buttons or markup + to convert into a markup. + + inline_only (`bool`, optional): + Whether the buttons **must** be inline buttons only or not. + + Example + .. code-block:: python + + from telethon import Button + + markup = client.build_reply_markup(Button.inline('hi')) + # later + await client.send_message(chat, 'click me', buttons=markup) + """ + return buttons.build_reply_markup(**locals()) + + + # endregion Buttons + + # region Chats + + def iter_participants( + self: 'TelegramClient', + entity: 'hints.EntityLike', + limit: float = None, + *, + search: str = '', + filter: 'types.TypeChannelParticipantsFilter' = None, + aggressive: bool = False) -> _ParticipantsIter: + """ + Iterator over the participants belonging to the specified chat. + + The order is unspecified. + + Arguments + entity (`entity`): + The entity from which to retrieve the participants list. + + limit (`int`): + Limits amount of participants fetched. + + search (`str`, optional): + Look for participants with this string in name/username. + + If ``aggressive is True``, the symbols from this string will + be used. + + filter (:tl:`ChannelParticipantsFilter`, optional): + The filter to be used, if you want e.g. only admins + Note that you might not have permissions for some filter. + This has no effect for normal chats or users. + + .. note:: + + The filter :tl:`ChannelParticipantsBanned` will return + *restricted* users. If you want *banned* users you should + use :tl:`ChannelParticipantsKicked` instead. + + aggressive (`bool`, optional): + Aggressively looks for all participants in the chat. + + This is useful for channels since 20 July 2018, + Telegram added a server-side limit where only the + first 200 members can be retrieved. With this flag + set, more than 200 will be often be retrieved. + + This has no effect if a ``filter`` is given. + + Yields + The :tl:`User` objects returned by :tl:`GetParticipantsRequest` + with an additional ``.participant`` attribute which is the + matched :tl:`ChannelParticipant` type for channels/megagroups + or :tl:`ChatParticipants` for normal chats. + + Example + .. code-block:: python + + # Show all user IDs in a chat + async for user in client.iter_participants(chat): + print(user.id) + + # Search by name + async for user in client.iter_participants(chat, search='name'): + print(user.username) + + # Filter by admins + from telethon.tl.types import ChannelParticipantsAdmins + async for user in client.iter_participants(chat, filter=ChannelParticipantsAdmins): + print(user.first_name) + """ + return chats.iter_participants(**locals()) + + async def get_participants( + self: 'TelegramClient', + *args, + **kwargs) -> 'hints.TotalList': + """ + Same as `iter_participants()`, but returns a + `TotalList ` instead. + + Example + .. code-block:: python + + users = await client.get_participants(chat) + print(users[0].first_name) + + for user in users: + if user.username is not None: + print(user.username) + """ + return chats.get_participants(*args, **kwargs) + + get_participants.__signature__ = inspect.signature(iter_participants) + + def iter_admin_log( + self: 'TelegramClient', + entity: 'hints.EntityLike', + limit: float = None, + *, + max_id: int = 0, + min_id: int = 0, + search: str = None, + admins: 'hints.EntitiesLike' = None, + join: bool = None, + leave: bool = None, + invite: bool = None, + restrict: bool = None, + unrestrict: bool = None, + ban: bool = None, + unban: bool = None, + promote: bool = None, + demote: bool = None, + info: bool = None, + settings: bool = None, + pinned: bool = None, + edit: bool = None, + delete: bool = None, + group_call: bool = None) -> _AdminLogIter: + """ + Iterator over the admin log for the specified channel. + + The default order is from the most recent event to to the oldest. + + Note that you must be an administrator of it to use this method. + + If none of the filters are present (i.e. they all are `None`), + *all* event types will be returned. If at least one of them is + `True`, only those that are true will be returned. + + Arguments + entity (`entity`): + The channel entity from which to get its admin log. + + limit (`int` | `None`, optional): + Number of events to be retrieved. + + The limit may also be `None`, which would eventually return + the whole history. + + max_id (`int`): + All the events with a higher (newer) ID or equal to this will + be excluded. + + min_id (`int`): + All the events with a lower (older) ID or equal to this will + be excluded. + + search (`str`): + The string to be used as a search query. + + admins (`entity` | `list`): + If present, the events will be filtered by these admins + (or single admin) and only those caused by them will be + returned. + + join (`bool`): + If `True`, events for when a user joined will be returned. + + leave (`bool`): + If `True`, events for when a user leaves will be returned. + + invite (`bool`): + If `True`, events for when a user joins through an invite + link will be returned. + + restrict (`bool`): + If `True`, events with partial restrictions will be + returned. This is what the API calls "ban". + + unrestrict (`bool`): + If `True`, events removing restrictions will be returned. + This is what the API calls "unban". + + ban (`bool`): + If `True`, events applying or removing all restrictions will + be returned. This is what the API calls "kick" (restricting + all permissions removed is a ban, which kicks the user). + + unban (`bool`): + If `True`, events removing all restrictions will be + returned. This is what the API calls "unkick". + + promote (`bool`): + If `True`, events with admin promotions will be returned. + + demote (`bool`): + If `True`, events with admin demotions will be returned. + + info (`bool`): + If `True`, events changing the group info will be returned. + + settings (`bool`): + If `True`, events changing the group settings will be + returned. + + pinned (`bool`): + If `True`, events of new pinned messages will be returned. + + edit (`bool`): + If `True`, events of message edits will be returned. + + delete (`bool`): + If `True`, events of message deletions will be returned. + + group_call (`bool`): + If `True`, events related to group calls will be returned. + + Yields + Instances of `AdminLogEvent `. + + Example + .. code-block:: python + + async for event in client.iter_admin_log(channel): + if event.changed_title: + print('The title changed from', event.old, 'to', event.new) + """ + return chats.iter_admin_log(**locals()) + + async def get_admin_log( + self: 'TelegramClient', + *args, + **kwargs) -> 'hints.TotalList': + """ + Same as `iter_admin_log()`, but returns a ``list`` instead. + + Example + .. code-block:: python + + # Get a list of deleted message events which said "heck" + events = await client.get_admin_log(channel, search='heck', delete=True) + + # Print the old message before it was deleted + print(events[0].old) + """ + return chats.get_admin_log(*args, **kwargs) + + get_admin_log.__signature__ = inspect.signature(iter_admin_log) + + def iter_profile_photos( + self: 'TelegramClient', + entity: 'hints.EntityLike', + limit: int = None, + *, + offset: int = 0, + max_id: int = 0) -> _ProfilePhotoIter: + """ + Iterator over a user's profile photos or a chat's photos. + + The order is from the most recent photo to the oldest. + + Arguments + entity (`entity`): + The entity from which to get the profile or chat photos. + + limit (`int` | `None`, optional): + Number of photos to be retrieved. + + The limit may also be `None`, which would eventually all + the photos that are still available. + + offset (`int`): + How many photos should be skipped before returning the first one. + + max_id (`int`): + The maximum ID allowed when fetching photos. + + Yields + Instances of :tl:`Photo`. + + Example + .. code-block:: python + + # Download all the profile photos of some user + async for photo in client.iter_profile_photos(user): + await client.download_media(photo) + """ + return chats.iter_profile_photos(**locals()) + + async def get_profile_photos( + self: 'TelegramClient', + *args, + **kwargs) -> 'hints.TotalList': + """ + Same as `iter_profile_photos()`, but returns a + `TotalList ` instead. + + Example + .. code-block:: python + + # Get the photos of a channel + photos = await client.get_profile_photos(channel) + + # Download the oldest photo + await client.download_media(photos[-1]) + """ + return chats.get_profile_photos(*args, **kwargs) + + get_profile_photos.__signature__ = inspect.signature(iter_profile_photos) + + def action( + self: 'TelegramClient', + entity: 'hints.EntityLike', + action: 'typing.Union[str, types.TypeSendMessageAction]', + *, + delay: float = 4, + auto_cancel: bool = True) -> 'typing.Union[_ChatAction, typing.Coroutine]': + """ + Returns a context-manager object to represent a "chat action". + + Chat actions indicate things like "user is typing", "user is + uploading a photo", etc. + + If the action is ``'cancel'``, you should just ``await`` the result, + since it makes no sense to use a context-manager for it. + + See the example below for intended usage. + + Arguments + entity (`entity`): + The entity where the action should be showed in. + + action (`str` | :tl:`SendMessageAction`): + The action to show. You can either pass a instance of + :tl:`SendMessageAction` or better, a string used while: + + * ``'typing'``: typing a text message. + * ``'contact'``: choosing a contact. + * ``'game'``: playing a game. + * ``'location'``: choosing a geo location. + * ``'sticker'``: choosing a sticker. + * ``'record-audio'``: recording a voice note. + You may use ``'record-voice'`` as alias. + * ``'record-round'``: recording a round video. + * ``'record-video'``: recording a normal video. + * ``'audio'``: sending an audio file (voice note or song). + You may use ``'voice'`` and ``'song'`` as aliases. + * ``'round'``: uploading a round video. + * ``'video'``: uploading a video file. + * ``'photo'``: uploading a photo. + * ``'document'``: uploading a document file. + You may use ``'file'`` as alias. + * ``'cancel'``: cancel any pending action in this chat. + + Invalid strings will raise a ``ValueError``. + + delay (`int` | `float`): + The delay, in seconds, to wait between sending actions. + For example, if the delay is 5 and it takes 7 seconds to + do something, three requests will be made at 0s, 5s, and + 7s to cancel the action. + + auto_cancel (`bool`): + Whether the action should be cancelled once the context + manager exists or not. The default is `True`, since + you don't want progress to be shown when it has already + completed. + + Returns + Either a context-manager object or a coroutine. + + Example + .. code-block:: python + + # Type for 2 seconds, then send a message + async with client.action(chat, 'typing'): + await asyncio.sleep(2) + await client.send_message(chat, 'Hello world! I type slow ^^') + + # Cancel any previous action + await client.action(chat, 'cancel') + + # Upload a document, showing its progress (most clients ignore this) + async with client.action(chat, 'document') as action: + await client.send_file(chat, zip_file, progress_callback=action.progress) + """ + return chats.action(**locals()) + + async def edit_admin( + self: 'TelegramClient', + entity: 'hints.EntityLike', + user: 'hints.EntityLike', + *, + change_info: bool = None, + post_messages: bool = None, + edit_messages: bool = None, + delete_messages: bool = None, + ban_users: bool = None, + invite_users: bool = None, + pin_messages: bool = None, + add_admins: bool = None, + manage_call: bool = None, + anonymous: bool = None, + is_admin: bool = None, + title: str = None) -> types.Updates: + """ + Edits admin permissions for someone in a chat. + + Raises an error if a wrong combination of rights are given + (e.g. you don't have enough permissions to grant one). + + Unless otherwise stated, permissions will work in channels and megagroups. + + Arguments + entity (`entity`): + The channel, megagroup or chat where the promotion should happen. + + user (`entity`): + The user to be promoted. + + change_info (`bool`, optional): + Whether the user will be able to change info. + + post_messages (`bool`, optional): + Whether the user will be able to post in the channel. + This will only work in broadcast channels. + + edit_messages (`bool`, optional): + Whether the user will be able to edit messages in the channel. + This will only work in broadcast channels. + + delete_messages (`bool`, optional): + Whether the user will be able to delete messages. + + ban_users (`bool`, optional): + Whether the user will be able to ban users. + + invite_users (`bool`, optional): + Whether the user will be able to invite users. Needs some testing. + + pin_messages (`bool`, optional): + Whether the user will be able to pin messages. + + add_admins (`bool`, optional): + Whether the user will be able to add admins. + + manage_call (`bool`, optional): + Whether the user will be able to manage group calls. + + anonymous (`bool`, optional): + Whether the user will remain anonymous when sending messages. + The sender of the anonymous messages becomes the group itself. + + .. note:: + + Users may be able to identify the anonymous admin by its + custom title, so additional care is needed when using both + ``anonymous`` and custom titles. For example, if multiple + anonymous admins share the same title, users won't be able + to distinguish them. + + is_admin (`bool`, optional): + Whether the user will be an admin in the chat. + This will only work in small group chats. + Whether the user will be an admin in the chat. This is the + only permission available in small group chats, and when + used in megagroups, all non-explicitly set permissions will + have this value. + + Essentially, only passing ``is_admin=True`` will grant all + permissions, but you can still disable those you need. + + title (`str`, optional): + The custom title (also known as "rank") to show for this admin. + This text will be shown instead of the "admin" badge. + This will only work in channels and megagroups. + + When left unspecified or empty, the default localized "admin" + badge will be shown. + + Returns + The resulting :tl:`Updates` object. + + Example + .. code-block:: python + + # Allowing `user` to pin messages in `chat` + await client.edit_admin(chat, user, pin_messages=True) + + # Granting all permissions except for `add_admins` + await client.edit_admin(chat, user, is_admin=True, add_admins=False) + """ + return chats.edit_admin(**locals()) + + async def edit_permissions( + self: 'TelegramClient', + entity: 'hints.EntityLike', + user: 'typing.Optional[hints.EntityLike]' = None, + until_date: 'hints.DateLike' = None, + *, + view_messages: bool = True, + send_messages: bool = True, + send_media: bool = True, + send_stickers: bool = True, + send_gifs: bool = True, + send_games: bool = True, + send_inline: bool = True, + embed_link_previews: bool = True, + send_polls: bool = True, + change_info: bool = True, + invite_users: bool = True, + pin_messages: bool = True) -> types.Updates: + """ + Edits user restrictions in a chat. + + Set an argument to `False` to apply a restriction (i.e. remove + the permission), or omit them to use the default `True` (i.e. + don't apply a restriction). + + Raises an error if a wrong combination of rights are given + (e.g. you don't have enough permissions to revoke one). + + By default, each boolean argument is `True`, meaning that it + is true that the user has access to the default permission + and may be able to make use of it. + + If you set an argument to `False`, then a restriction is applied + regardless of the default permissions. + + It is important to note that `True` does *not* mean grant, only + "don't restrict", and this is where the default permissions come + in. A user may have not been revoked the ``pin_messages`` permission + (it is `True`) but they won't be able to use it if the default + permissions don't allow it either. + + Arguments + entity (`entity`): + The channel or megagroup where the restriction should happen. + + user (`entity`, optional): + If specified, the permission will be changed for the specific user. + If left as `None`, the default chat permissions will be updated. + + until_date (`DateLike`, optional): + When the user will be unbanned. + + If the due date or duration is longer than 366 days or shorter than + 30 seconds, the ban will be forever. Defaults to ``0`` (ban forever). + + view_messages (`bool`, optional): + Whether the user is able to view messages or not. + Forbidding someone from viewing messages equals to banning them. + This will only work if ``user`` is set. + + send_messages (`bool`, optional): + Whether the user is able to send messages or not. + + send_media (`bool`, optional): + Whether the user is able to send media or not. + + send_stickers (`bool`, optional): + Whether the user is able to send stickers or not. + + send_gifs (`bool`, optional): + Whether the user is able to send animated gifs or not. + + send_games (`bool`, optional): + Whether the user is able to send games or not. + + send_inline (`bool`, optional): + Whether the user is able to use inline bots or not. + + embed_link_previews (`bool`, optional): + Whether the user is able to enable the link preview in the + messages they send. Note that the user will still be able to + send messages with links if this permission is removed, but + these links won't display a link preview. + + send_polls (`bool`, optional): + Whether the user is able to send polls or not. + + change_info (`bool`, optional): + Whether the user is able to change info or not. + + invite_users (`bool`, optional): + Whether the user is able to invite other users or not. + + pin_messages (`bool`, optional): + Whether the user is able to pin messages or not. + + Returns + The resulting :tl:`Updates` object. + + Example + .. code-block:: python + + from datetime import timedelta + + # Banning `user` from `chat` for 1 minute + await client.edit_permissions(chat, user, timedelta(minutes=1), + view_messages=False) + + # Banning `user` from `chat` forever + await client.edit_permissions(chat, user, view_messages=False) + + # Kicking someone (ban + un-ban) + await client.edit_permissions(chat, user, view_messages=False) + await client.edit_permissions(chat, user) + """ + return chats.edit_permissions(**locals()) + + async def kick_participant( + self: 'TelegramClient', + entity: 'hints.EntityLike', + user: 'typing.Optional[hints.EntityLike]' + ): + """ + Kicks a user from a chat. + + Kicking yourself (``'me'``) will result in leaving the chat. + + .. note:: + + Attempting to kick someone who was banned will remove their + restrictions (and thus unbanning them), since kicking is just + ban + unban. + + Arguments + entity (`entity`): + The channel or chat where the user should be kicked from. + + user (`entity`, optional): + The user to kick. + + Returns + Returns the service `Message ` + produced about a user being kicked, if any. + + Example + .. code-block:: python + + # Kick some user from some chat, and deleting the service message + msg = await client.kick_participant(chat, user) + await msg.delete() + + # Leaving chat + await client.kick_participant(chat, 'me') + """ + return chats.kick_participant(**locals()) + + async def get_permissions( + self: 'TelegramClient', + entity: 'hints.EntityLike', + user: 'hints.EntityLike' = None + ) -> 'typing.Optional[custom.ParticipantPermissions]': + """ + Fetches the permissions of a user in a specific chat or channel or + get Default Restricted Rights of Chat or Channel. + + .. note:: + + This request has to fetch the entire chat for small group chats, + which can get somewhat expensive, so use of a cache is advised. + + Arguments + entity (`entity`): + The channel or chat the user is participant of. + + user (`entity`, optional): + Target user. + + Returns + A `ParticipantPermissions ` + instance. Refer to its documentation to see what properties are + available. + + Example + .. code-block:: python + + permissions = await client.get_permissions(chat, user) + if permissions.is_admin: + # do something + + # Get Banned Permissions of Chat + await client.get_permissions(chat) + """ + return chats.get_permissions(**locals()) + + async def get_stats( + self: 'TelegramClient', + entity: 'hints.EntityLike', + message: 'typing.Union[int, types.Message]' = None, + ): + """ + Retrieves statistics from the given megagroup or broadcast channel. + + Note that some restrictions apply before being able to fetch statistics, + in particular the channel must have enough members (for megagroups, this + requires `at least 500 members`_). + + Arguments + entity (`entity`): + The channel from which to get statistics. + + message (`int` | ``Message``, optional): + The message ID from which to get statistics, if your goal is + to obtain the statistics of a single message. + + Raises + If the given entity is not a channel (broadcast or megagroup), + a `TypeError` is raised. + + If there are not enough members (poorly named) errors such as + ``telethon.errors.ChatAdminRequiredError`` will appear. + + Returns + If both ``entity`` and ``message`` were provided, returns + :tl:`MessageStats`. Otherwise, either :tl:`BroadcastStats` or + :tl:`MegagroupStats`, depending on whether the input belonged to a + broadcast channel or megagroup. + + Example + .. code-block:: python + + # Some megagroup or channel username or ID to fetch + channel = -100123 + stats = await client.get_stats(channel) + print('Stats from', stats.period.min_date, 'to', stats.period.max_date, ':') + print(stats.stringify()) + + .. _`at least 500 members`: https://telegram.org/blog/profile-videos-people-nearby-and-more + """ + return chats.get_stats(**locals()) + + # endregion Chats + + # region Dialogs + + 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). + + 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``. + + offset_date (`datetime`, optional): + The offset date to be used. + + offset_id (`int`, optional): + The message ID to be used as an offset. + + offset_peer (:tl:`InputPeer`, optional): + The peer to be used as an offset. + + ignore_pinned (`bool`, optional): + Whether pinned dialogs should be ignored or not. + When set to `True`, these won't be yielded at all. + + 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. + + folder (`int`, optional): + The folder from which the dialogs should be retrieved. + + If left unspecified, all dialogs (including those from + folders) will be returned. + + If set to ``0``, all dialogs that don't belong to any + folder will be returned. + + If set to a folder number like ``1``, only those from + said folder will be returned. + + 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 `. + + 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)) + """ + return dialogs.iter_dialogs(**locals()) + + async def get_dialogs(self: 'TelegramClient', *args, **kwargs) -> 'hints.TotalList': + """ + Same as `iter_dialogs()`, but returns a + `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 dialogs.get_dialogs(*args, **kwargs) + + 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 `. + + 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) + """ + return dialogs.iter_drafts(**locals()) + + 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) + """ + return dialogs.get_drafts(**locals()) + + 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) + """ + return dialogs.edit_folder(**locals()) + + 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() `. + + 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') + """ + return dialogs.delete_dialog(**locals()) + + 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 ` + with the given entity. + + .. 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 + ` + and a subsequent call to `conv.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 `. + + Example + .. code-block:: python + + # denotes outgoing messages you sent + # denotes incoming response messages + with bot.conversation(chat) as conv: + # Hi! + conv.send_message('Hi!') + + # Hello! + hello = conv.get_response() + + # Please tell me your name + conv.send_message('Please tell me your name') + + # ? + name = conv.get_response().raw_text + + while not any(x.isalpha() for x in name): + # Your name didn't have any letters! Try again + conv.send_message("Your name didn't have any letters! Try again") + + # Human + name = conv.get_response().raw_text + + # Thanks Human! + conv.send_message('Thanks {}!'.format(name)) + """ + return dialogs.conversation(**locals()) + + # endregion Dialogs + + # region Downloads + + async def download_profile_photo( + self: 'TelegramClient', + entity: 'hints.EntityLike', + file: 'hints.FileLike' = None, + *, + download_big: bool = True) -> typing.Optional[str]: + """ + Downloads the profile photo from the given user, chat or channel. + + Arguments + entity (`entity`): + From who the photo will be downloaded. + + .. note:: + + This method expects the full entity (which has the data + to download the photo), not an input variant. + + It's possible that sometimes you can't fetch the entity + from its input (since you can get errors like + ``ChannelPrivateError``) but you already have it through + another call, like getting a forwarded message from it. + + file (`str` | `file`, optional): + The output file path, directory, or stream-like object. + If the path exists and is a file, it will be overwritten. + If file is the type `bytes`, it will be downloaded in-memory + as a bytestring (e.g. ``file=bytes``). + + download_big (`bool`, optional): + Whether to use the big version of the available photos. + + Returns + `None` if no photo was provided, or if it was Empty. On success + the file path is returned since it may differ from the one given. + + Example + .. code-block:: python + + # Download your own profile photo + path = await client.download_profile_photo('me') + print(path) + """ + return downloads.download_profile_photo(**locals()) + + async def download_media( + self: 'TelegramClient', + message: 'hints.MessageLike', + file: 'hints.FileLike' = None, + *, + thumb: 'typing.Union[int, types.TypePhotoSize]' = None, + progress_callback: 'hints.ProgressCallback' = None) -> typing.Optional[typing.Union[str, bytes]]: + """ + Downloads the given media from a message object. + + Note that if the download is too slow, you should consider installing + ``cryptg`` (through ``pip install cryptg``) so that decrypting the + received data is done in C instead of Python (much faster). + + See also `Message.download_media() `. + + Arguments + message (`Message ` | :tl:`Media`): + The media or message containing the media that will be downloaded. + + file (`str` | `file`, optional): + The output file path, directory, or stream-like object. + If the path exists and is a file, it will be overwritten. + If file is the type `bytes`, it will be downloaded in-memory + as a bytestring (e.g. ``file=bytes``). + + progress_callback (`callable`, optional): + A callback function accepting two parameters: + ``(received bytes, total)``. + + thumb (`int` | :tl:`PhotoSize`, optional): + Which thumbnail size from the document or photo to download, + instead of downloading the document or photo itself. + + If it's specified but the file does not have a thumbnail, + this method will return `None`. + + The parameter should be an integer index between ``0`` and + ``len(sizes)``. ``0`` will download the smallest thumbnail, + and ``len(sizes) - 1`` will download the largest thumbnail. + You can also use negative indices, which work the same as + they do in Python's `list`. + + You can also pass the :tl:`PhotoSize` instance to use. + Alternatively, the thumb size type `str` may be used. + + In short, use ``thumb=0`` if you want the smallest thumbnail + and ``thumb=-1`` if you want the largest thumbnail. + + .. note:: + The largest thumbnail may be a video instead of a photo, + as they are available since layer 116 and are bigger than + any of the photos. + + Returns + `None` if no media was provided, or if it was Empty. On success + the file path is returned since it may differ from the one given. + + Example + .. code-block:: python + + path = await client.download_media(message) + await client.download_media(message, filename) + # or + path = await message.download_media() + await message.download_media(filename) + + # Printing download progress + def callback(current, total): + print('Downloaded', current, 'out of', total, + 'bytes: {:.2%}'.format(current / total)) + + await client.download_media(message, progress_callback=callback) + """ + return downloads.download_media(**locals()) + + async def download_file( + self: 'TelegramClient', + input_location: 'hints.FileLike', + file: 'hints.OutFileLike' = None, + *, + part_size_kb: float = None, + file_size: int = None, + progress_callback: 'hints.ProgressCallback' = None, + dc_id: int = None, + key: bytes = None, + iv: bytes = None) -> typing.Optional[bytes]: + """ + Low-level method to download files from their input location. + + .. note:: + + Generally, you should instead use `download_media`. + This method is intended to be a bit more low-level. + + Arguments + input_location (:tl:`InputFileLocation`): + The file location from which the file will be downloaded. + See `telethon.utils.get_input_location` source for a complete + list of supported types. + + file (`str` | `file`, optional): + The output file path, directory, or stream-like object. + If the path exists and is a file, it will be overwritten. + + If the file path is `None` or `bytes`, then the result + will be saved in memory and returned as `bytes`. + + part_size_kb (`int`, optional): + Chunk size when downloading files. The larger, the less + requests will be made (up to 512KB maximum). + + file_size (`int`, optional): + The file size that is about to be downloaded, if known. + Only used if ``progress_callback`` is specified. + + progress_callback (`callable`, optional): + A callback function accepting two parameters: + ``(downloaded bytes, total)``. Note that the + ``total`` is the provided ``file_size``. + + dc_id (`int`, optional): + The data center the library should connect to in order + to download the file. You shouldn't worry about this. + + key ('bytes', optional): + In case of an encrypted upload (secret chats) a key is supplied + + iv ('bytes', optional): + In case of an encrypted upload (secret chats) an iv is supplied + + + Example + .. code-block:: python + + # Download a file and print its header + data = await client.download_file(input_file, bytes) + print(data[:16]) + """ + return downloads.download_file(**locals()) + + def iter_download( + self: 'TelegramClient', + file: 'hints.FileLike', + *, + offset: int = 0, + stride: int = None, + limit: int = None, + chunk_size: int = None, + request_size: int = MAX_CHUNK_SIZE, + file_size: int = None, + dc_id: int = None + ): + """ + Iterates over a file download, yielding chunks of the file. + + This method can be used to stream files in a more convenient + way, since it offers more control (pausing, resuming, etc.) + + .. note:: + + Using a value for `offset` or `stride` which is not a multiple + of the minimum allowed `request_size`, or if `chunk_size` is + different from `request_size`, the library will need to do a + bit more work to fetch the data in the way you intend it to. + + You normally shouldn't worry about this. + + Arguments + file (`hints.FileLike`): + The file of which contents you want to iterate over. + + offset (`int`, optional): + The offset in bytes into the file from where the + download should start. For example, if a file is + 1024KB long and you just want the last 512KB, you + would use ``offset=512 * 1024``. + + stride (`int`, optional): + The stride of each chunk (how much the offset should + advance between reading each chunk). This parameter + should only be used for more advanced use cases. + + It must be bigger than or equal to the `chunk_size`. + + limit (`int`, optional): + The limit for how many *chunks* will be yielded at most. + + chunk_size (`int`, optional): + The maximum size of the chunks that will be yielded. + Note that the last chunk may be less than this value. + By default, it equals to `request_size`. + + request_size (`int`, optional): + How many bytes will be requested to Telegram when more + data is required. By default, as many bytes as possible + are requested. If you would like to request data in + smaller sizes, adjust this parameter. + + Note that values outside the valid range will be clamped, + and the final value will also be a multiple of the minimum + allowed size. + + file_size (`int`, optional): + If the file size is known beforehand, you should set + this parameter to said value. Depending on the type of + the input file passed, this may be set automatically. + + dc_id (`int`, optional): + The data center the library should connect to in order + to download the file. You shouldn't worry about this. + + Yields + + `bytes` objects representing the chunks of the file if the + right conditions are met, or `memoryview` objects instead. + + Example + .. code-block:: python + + # Streaming `media` to an output file + # After the iteration ends, the sender is cleaned up + with open('photo.jpg', 'wb') as fd: + async for chunk in client.iter_download(media): + fd.write(chunk) + + # Fetching only the header of a file (32 bytes) + # You should manually close the iterator in this case. + # + # "stream" is a common name for asynchronous generators, + # and iter_download will yield `bytes` (chunks of the file). + stream = client.iter_download(media, request_size=32) + header = await stream.__anext__() # "manual" version of `async for` + await stream.close() + assert len(header) == 32 + """ + return downloads.iter_download(**locals()) + + # endregion Downloads + + # region Message parse + + @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. + + When setting a different value it should be one of: + + * 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 ``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])``. + + The ``unparse`` method should be the inverse of ``parse`` such + that ``assert text == unparse(*parse(text))``. + + See :tl:`MessageEntity` for allowed message entities. + + Example + .. code-block:: python + + # Disabling default formatting + client.parse_mode = None + + # Enabling HTML as the default format + client.parse_mode = 'html' + """ + return messageparse.get_parse_mode(**locals()) + + @parse_mode.setter + def parse_mode(self: 'TelegramClient', mode: str): + return messageparse.set_parse_mode(**locals()) + + # endregion Message parse + + # region Messages + + def iter_messages( + self: 'TelegramClient', + entity: 'hints.EntityLike', + limit: float = None, + *, + offset_date: 'hints.DateLike' = None, + offset_id: int = 0, + max_id: int = 0, + min_id: int = 0, + add_offset: int = 0, + search: str = None, + filter: 'typing.Union[types.TypeMessagesFilter, typing.Type[types.TypeMessagesFilter]]' = None, + from_user: 'hints.EntityLike' = None, + wait_time: float = None, + ids: 'typing.Union[int, typing.Sequence[int]]' = None, + reverse: bool = False, + reply_to: int = None, + scheduled: bool = False + ) -> 'typing.Union[_MessagesIter, _IDsIter]': + """ + Iterator over the messages for the given chat. + + The default order is from newest to oldest, but this + behaviour can be changed with the `reverse` parameter. + + If either `search`, `filter` or `from_user` are provided, + :tl:`messages.Search` will be used instead of :tl:`messages.getHistory`. + + .. note:: + + Telegram's flood wait limit for :tl:`GetHistoryRequest` seems to + be around 30 seconds per 10 requests, therefore a sleep of 1 + second is the default for this limit (or above). + + Arguments + entity (`entity`): + The entity from whom to retrieve the message history. + + It may be `None` to perform a global search, or + to get messages by their ID from no particular chat. + Note that some of the offsets will not work if this + is the case. + + Note that if you want to perform a global search, + you **must** set a non-empty `search` string, a `filter`. + or `from_user`. + + limit (`int` | `None`, optional): + Number of messages to be retrieved. Due to limitations with + the API retrieving more than 3000 messages will take longer + than half a minute (or even more based on previous calls). + + The limit may also be `None`, which would eventually return + the whole history. + + offset_date (`datetime`): + Offset date (messages *previous* to this date will be + retrieved). Exclusive. + + offset_id (`int`): + Offset message ID (only messages *previous* to the given + ID will be retrieved). Exclusive. + + max_id (`int`): + All the messages with a higher (newer) ID or equal to this will + be excluded. + + min_id (`int`): + All the messages with a lower (older) ID or equal to this will + be excluded. + + add_offset (`int`): + 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 entity will be returned. + + wait_time (`int`): + Wait time (in seconds) between different + :tl:`GetHistoryRequest`. Use this parameter to avoid hitting + the ``FloodWaitError`` as needed. If left to `None`, it will + default to 1 second only if the limit is higher than 3000. + + If the ``ids`` parameter is used, this time will default + to 10 seconds only if the amount of IDs is higher than 300. + + ids (`int`, `list`): + A single integer ID (or several IDs) for the message that + should be returned. This parameter takes precedence over + the rest (which will be ignored if this is set). This can + for instance be used to get the message with ID 123 from + a channel. Note that if the message doesn't exist, `None` + will appear in its place, so that zipping the list of IDs + with the messages can match one-to-one. + + .. note:: + + At the time of writing, Telegram will **not** return + :tl:`MessageEmpty` for :tl:`InputMessageReplyTo` IDs that + failed (i.e. the message is not replying to any, or is + replying to a deleted message). This means that it is + **not** possible to match messages one-by-one, so be + careful if you use non-integers in this parameter. + + reverse (`bool`, optional): + If set to `True`, the messages will be returned in reverse + order (from oldest to newest, instead of the default newest + to oldest). This also means that the meaning of `offset_id` + and `offset_date` parameters is reversed, although they will + still be exclusive. `min_id` becomes equivalent to `offset_id` + instead of being `max_id` as well since messages are returned + in ascending order. + + You cannot use this if both `entity` and `ids` are `None`. + + reply_to (`int`, optional): + If set to a message ID, the messages that reply to this ID + will be returned. This feature is also known as comments in + posts of broadcast channels, or viewing threads in groups. + + This feature can only be used in broadcast channels and their + linked megagroups. Using it in a chat or private conversation + will result in ``telethon.errors.PeerIdInvalidError`` to occur. + + When using this parameter, the ``filter`` and ``search`` + parameters have no effect, since Telegram's API doesn't + support searching messages in replies. + + .. note:: + + This feature is used to get replies to a message in the + *discussion* group. If the same broadcast channel sends + a message and replies to it itself, that reply will not + be included in the results. + + scheduled (`bool`, optional): + If set to `True`, messages which are scheduled will be returned. + All other parameter will be ignored for this, except `entity`. + + Yields + Instances of `Message `. + + Example + .. code-block:: python + + # From most-recent to oldest + async for message in client.iter_messages(chat): + print(message.id, message.text) + + # From oldest to most-recent + async for message in client.iter_messages(chat, reverse=True): + print(message.id, message.text) + + # Filter by sender + async for message in client.iter_messages(chat, from_user='me'): + print(message.text) + + # Server-side search with fuzzy text + async for message in client.iter_messages(chat, search='hello'): + print(message.id) + + # Filter by message type: + from telethon.tl.types import InputMessagesFilterPhotos + async for message in client.iter_messages(chat, filter=InputMessagesFilterPhotos): + print(message.photo) + + # Getting comments from a post in a channel: + async for message in client.iter_messages(channel, reply_to=123): + print(message.chat.title, message.text) + """ + return messages.iter_messages(**locals()) + + async def get_messages(self: 'TelegramClient', *args, **kwargs) -> 'hints.TotalList': + """ + Same as `iter_messages()`, but returns a + `TotalList ` instead. + + If the `limit` is not set, it will be 1 by default unless both + `min_id` **and** `max_id` are set (as *named* arguments), in + which case the entire range will be returned. + + This is so because any integer limit would be rather arbitrary and + it's common to only want to fetch one message, but if a range is + specified it makes sense that it should return the entirety of it. + + If `ids` is present in the *named* arguments and is not a list, + a single `Message ` will be + returned for convenience instead of a list. + + Example + .. code-block:: python + + # Get 0 photos and print the total to show how many photos there are + from telethon.tl.types import InputMessagesFilterPhotos + photos = await client.get_messages(chat, 0, filter=InputMessagesFilterPhotos) + print(photos.total) + + # Get all the photos + photos = await client.get_messages(chat, None, filter=InputMessagesFilterPhotos) + + # Get messages by ID: + message_1337 = await client.get_messages(chat, ids=1337) + """ + return messages.get_messages(**locals()) + + get_messages.__signature__ = inspect.signature(iter_messages) + + async def send_message( + self: 'TelegramClient', + entity: 'hints.EntityLike', + message: 'hints.MessageLike' = '', + *, + reply_to: 'typing.Union[int, types.Message]' = None, + attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None, + parse_mode: typing.Optional[str] = (), + formatting_entities: typing.Optional[typing.List[types.TypeMessageEntity]] = None, + link_preview: bool = True, + file: 'typing.Union[hints.FileLike, typing.Sequence[hints.FileLike]]' = None, + thumb: 'hints.FileLike' = None, + force_document: bool = False, + clear_draft: bool = False, + buttons: 'hints.MarkupLike' = None, + silent: bool = None, + background: bool = None, + supports_streaming: bool = False, + schedule: 'hints.DateLike' = None, + comment_to: 'typing.Union[int, types.Message]' = None + ) -> 'types.Message': + """ + Sends a message to the specified user, chat or channel. + + The default parse mode is the same as the official applications + (a custom flavour of markdown). ``**bold**, `code` or __italic__`` + are available. In addition you can send ``[links](https://example.com)`` + and ``[mentions](@username)`` (or using IDs like in the Bot API: + ``[mention](tg://user?id=123456789)``) and ``pre`` blocks with three + backticks. + + Sending a ``/start`` command with a parameter (like ``?start=data``) + is also done through this method. Simply send ``'/start data'`` to + the bot. + + See also `Message.respond() ` + and `Message.reply() `. + + Arguments + entity (`entity`): + To who will it be sent. + + message (`str` | `Message `): + The message to be sent, or another message object to resend. + + The maximum length for a message is 35,000 bytes or 4,096 + characters. Longer messages will not be sliced automatically, + and you should slice them manually if the text to send is + longer than said length. + + reply_to (`int` | `Message `, optional): + Whether to reply to a message or not. If an integer is provided, + it should be the ID of the message that it should reply to. + + attributes (`list`, optional): + Optional attributes that override the inferred ones, like + :tl:`DocumentAttributeFilename` and so on. + + parse_mode (`object`, optional): + See the `TelegramClient.parse_mode + ` + property for allowed values. Markdown parsing will be used by + default. + + formatting_entities (`list`, optional): + A list of message formatting entities. When provided, the ``parse_mode`` is ignored. + + link_preview (`bool`, optional): + Should the link preview be shown? + + file (`file`, optional): + Sends a message with a file attached (e.g. a photo, + video, audio or document). The ``message`` may be empty. + + thumb (`str` | `bytes` | `file`, optional): + Optional JPEG thumbnail (for documents). **Telegram will + ignore this parameter** unless you pass a ``.jpg`` file! + The file must also be small in dimensions and in disk size. + Successful thumbnails were files below 20kB and 320x320px. + Width/height and dimensions/size ratios may be important. + For Telegram to accept a thumbnail, you must provide the + dimensions of the underlying media through ``attributes=`` + with :tl:`DocumentAttributesVideo` or by installing the + optional ``hachoir`` dependency. + + force_document (`bool`, optional): + Whether to send the given file as a document or not. + + clear_draft (`bool`, optional): + Whether the existing draft should be cleared or not. + + buttons (`list`, `custom.Button `, :tl:`KeyboardButton`): + The matrix (list of lists), row list or button to be shown + after sending the message. This parameter will only work if + you have signed in as a bot. You can also pass your own + :tl:`ReplyMarkup` here. + + All the following limits apply together: + + * There can be 100 buttons at most (any more are ignored). + * There can be 8 buttons per row at most (more are ignored). + * The maximum callback data per button is 64 bytes. + * The maximum data that can be embedded in total is just + over 4KB, shared between inline callback data and text. + + silent (`bool`, optional): + Whether the message should notify people in a broadcast + channel or not. Defaults to `False`, which means it will + notify them. Set it to `True` to alter this behaviour. + + background (`bool`, optional): + Whether the message should be send in background. + + supports_streaming (`bool`, optional): + Whether the sent video supports streaming or not. Note that + Telegram only recognizes as streamable some formats like MP4, + and others like AVI or MKV will not work. You should convert + these to MP4 before sending if you want them to be streamable. + Unsupported formats will result in ``VideoContentTypeError``. + + schedule (`hints.DateLike`, optional): + If set, the message won't send immediately, and instead + it will be scheduled to be automatically sent at a later + time. + + comment_to (`int` | `Message `, optional): + Similar to ``reply_to``, but replies in the linked group of a + broadcast channel instead (effectively leaving a "comment to" + the specified message). + + This parameter takes precedence over ``reply_to``. If there is + no linked chat, `telethon.errors.sgIdInvalidError` is raised. + + Returns + The sent `custom.Message `. + + Example + .. code-block:: python + + # Markdown is the default + await client.send_message('me', 'Hello **world**!') + + # Default to another parse mode + client.parse_mode = 'html' + + await client.send_message('me', 'Some bold and italic text') + await client.send_message('me', 'An URL') + # code and pre tags also work, but those break the documentation :) + await client.send_message('me', 'Mentions') + + # Explicit parse mode + # No parse mode by default + client.parse_mode = None + + # ...but here I want markdown + await client.send_message('me', 'Hello, **world**!', parse_mode='md') + + # ...and here I need HTML + await client.send_message('me', 'Hello, world!', parse_mode='html') + + # If you logged in as a bot account, you can send buttons + from telethon import events, Button + + @client.on(events.CallbackQuery) + async def callback(event): + await event.edit('Thank you for clicking {}!'.format(event.data)) + + # Single inline button + await client.send_message(chat, 'A single button, with "clk1" as data', + buttons=Button.inline('Click me', b'clk1')) + + # Matrix of inline buttons + await client.send_message(chat, 'Pick one from this grid', buttons=[ + [Button.inline('Left'), Button.inline('Right')], + [Button.url('Check this site!', 'https://example.com')] + ]) + + # Reply keyboard + await client.send_message(chat, 'Welcome', buttons=[ + Button.text('Thanks!', resize=True, single_use=True), + Button.request_phone('Send phone'), + Button.request_location('Send location') + ]) + + # Forcing replies or clearing buttons. + await client.send_message(chat, 'Reply to me', buttons=Button.force_reply()) + await client.send_message(chat, 'Bye Keyboard!', buttons=Button.clear()) + + # Scheduling a message to be sent after 5 minutes + from datetime import timedelta + await client.send_message(chat, 'Hi, future!', schedule=timedelta(minutes=5)) + """ + return messages.send_message(**locals()) + + async def forward_messages( + self: 'TelegramClient', + entity: 'hints.EntityLike', + messages: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]', + from_peer: 'hints.EntityLike' = None, + *, + background: bool = None, + with_my_score: bool = None, + silent: bool = None, + as_album: bool = None, + schedule: 'hints.DateLike' = None + ) -> 'typing.Sequence[types.Message]': + """ + Forwards the given messages to the specified entity. + + If you want to "forward" a message without the forward header + (the "forwarded from" text), you should use `send_message` with + the original message instead. This will send a copy of it. + + See also `Message.forward_to() `. + + Arguments + entity (`entity`): + To which entity the message(s) will be forwarded. + + messages (`list` | `int` | `Message `): + The message(s) to forward, or their integer IDs. + + from_peer (`entity`): + If the given messages are integer IDs and not instances + of the ``Message`` class, this *must* be specified in + order for the forward to work. This parameter indicates + the entity from which the messages should be forwarded. + + silent (`bool`, optional): + Whether the message should notify people with sound or not. + Defaults to `False` (send with a notification sound unless + the person has the chat muted). Set it to `True` to alter + this behaviour. + + background (`bool`, optional): + Whether the message should be forwarded in background. + + with_my_score (`bool`, optional): + Whether forwarded should contain your game score. + + as_album (`bool`, optional): + This flag no longer has any effect. + + schedule (`hints.DateLike`, optional): + If set, the message(s) won't forward immediately, and + instead they will be scheduled to be automatically sent + at a later time. + + Returns + The list of forwarded `Message `, + or a single one if a list wasn't provided as input. + + Note that if all messages are invalid (i.e. deleted) the call + will fail with ``MessageIdInvalidError``. If only some are + invalid, the list will have `None` instead of those messages. + + Example + .. code-block:: python + + # a single one + await client.forward_messages(chat, message) + # or + await client.forward_messages(chat, message_id, from_chat) + # or + await message.forward_to(chat) + + # multiple + await client.forward_messages(chat, messages) + # or + await client.forward_messages(chat, message_ids, from_chat) + + # Forwarding as a copy + await client.send_message(chat, message) + """ + return messages.forward_messages(**locals()) + + async def edit_message( + self: 'TelegramClient', + entity: 'typing.Union[hints.EntityLike, types.Message]', + message: 'hints.MessageLike' = None, + text: str = None, + *, + parse_mode: str = (), + attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None, + formatting_entities: typing.Optional[typing.List[types.TypeMessageEntity]] = None, + link_preview: bool = True, + file: 'hints.FileLike' = None, + thumb: 'hints.FileLike' = None, + force_document: bool = False, + buttons: 'hints.MarkupLike' = None, + supports_streaming: bool = False, + schedule: 'hints.DateLike' = None + ) -> 'types.Message': + """ + Edits the given message to change its text or media. + + See also `Message.edit() `. + + Arguments + entity (`entity` | `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. + + You may also pass a :tl:`InputBotInlineMessageID`, + which is the only way to edit messages that were sent + after the user selects an inline query result. + + message (`int` | `Message ` | `str`): + The ID of the message (or `Message + ` itself) to be edited. + If the `entity` was a `Message + `, then this message + will be treated as the new text. + + text (`str`, optional): + The new text of the message. Does nothing if the `entity` + was a `Message `. + + parse_mode (`object`, optional): + See the `TelegramClient.parse_mode + ` + property for allowed values. Markdown parsing will be used by + default. + + attributes (`list`, optional): + Optional attributes that override the inferred ones, like + :tl:`DocumentAttributeFilename` and so on. + + formatting_entities (`list`, optional): + A list of message formatting entities. When provided, the ``parse_mode`` is ignored. + + link_preview (`bool`, optional): + Should the link preview be shown? + + file (`str` | `bytes` | `file` | `media`, optional): + The file object that should replace the existing media + in the message. + + thumb (`str` | `bytes` | `file`, optional): + Optional JPEG thumbnail (for documents). **Telegram will + ignore this parameter** unless you pass a ``.jpg`` file! + The file must also be small in dimensions and in disk size. + Successful thumbnails were files below 20kB and 320x320px. + Width/height and dimensions/size ratios may be important. + For Telegram to accept a thumbnail, you must provide the + dimensions of the underlying media through ``attributes=`` + with :tl:`DocumentAttributesVideo` or by installing the + optional ``hachoir`` dependency. + + force_document (`bool`, optional): + Whether to send the given file as a document or not. + + buttons (`list`, `custom.Button `, :tl:`KeyboardButton`): + The matrix (list of lists), row list or button to be shown + after sending the message. This parameter will only work if + you have signed in as a bot. You can also pass your own + :tl:`ReplyMarkup` here. + + supports_streaming (`bool`, optional): + Whether the sent video supports streaming or not. Note that + Telegram only recognizes as streamable some formats like MP4, + and others like AVI or MKV will not work. You should convert + these to MP4 before sending if you want them to be streamable. + Unsupported formats will result in ``VideoContentTypeError``. + + schedule (`hints.DateLike`, optional): + If set, the message won't be edited immediately, and instead + it will be scheduled to be automatically edited at a later + time. + + Note that this parameter will have no effect if you are + trying to edit a message that was sent via inline bots. + + Returns + The edited `Message `, + unless `entity` was a :tl:`InputBotInlineMessageID` in which + case this method returns a boolean. + + Raises + ``MessageAuthorRequiredError`` if you're not the author of the + message but tried editing it anyway. + + ``MessageNotModifiedError`` if the contents of the message were + not modified at all. + + ``MessageIdInvalidError`` if the ID of the message is invalid + (the ID itself may be correct, but the message with that ID + cannot be edited). For example, when trying to edit messages + with a reply markup (or clear markup) this error will be raised. + + Example + .. code-block:: python + + message = await client.send_message(chat, 'hello') + + await client.edit_message(chat, message, 'hello!') + # or + await client.edit_message(chat, message.id, 'hello!!') + # or + await client.edit_message(message, 'hello!!!') + """ + return messages.edit_message(**locals()) + + async def delete_messages( + self: 'TelegramClient', + entity: 'hints.EntityLike', + message_ids: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]', + *, + revoke: bool = True) -> 'typing.Sequence[types.messages.AffectedMessages]': + """ + Deletes the given messages, optionally "for everyone". + + See also `Message.delete() `. + + .. warning:: + + This method does **not** validate that the message IDs belong + to the chat that you passed! It's possible for the method to + delete messages from different private chats and small group + chats at once, so make sure to pass the right IDs. + + Arguments + entity (`entity`): + From who the message will be deleted. This can actually + be `None` for normal chats, but **must** be present + for channels and megagroups. + + message_ids (`list` | `int` | `Message `): + The IDs (or ID) or messages to be deleted. + + revoke (`bool`, optional): + Whether the message should be deleted for everyone or not. + By default it has the opposite behaviour of official clients, + and it will delete the message for everyone. + + `Since 24 March 2019 + `_, you can + also revoke messages of any age (i.e. messages sent long in + the past) the *other* person sent in private conversations + (and of course your messages too). + + Disabling this has no effect on channels or megagroups, + since it will unconditionally delete the message for everyone. + + Returns + A list of :tl:`AffectedMessages`, each item being the result + for the delete calls of the messages in chunks of 100 each. + + Example + .. code-block:: python + + await client.delete_messages(chat, messages) + """ + return messages.delete_messages(**locals()) + + async def send_read_acknowledge( + self: 'TelegramClient', + entity: 'hints.EntityLike', + message: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]' = None, + *, + max_id: int = None, + clear_mentions: bool = False) -> bool: + """ + Marks messages as read and optionally clears mentions. + + This effectively marks a message as read (or more than one) in the + given conversation. + + If neither message nor maximum ID are provided, all messages will be + marked as read by assuming that ``max_id = 0``. + + If a message or maximum ID is provided, all the messages up to and + including such ID will be marked as read (for all messages whose ID + ≤ max_id). + + See also `Message.mark_read() `. + + Arguments + entity (`entity`): + The chat where these messages are located. + + message (`list` | `Message `): + Either a list of messages or a single message. + + max_id (`int`): + Until which message should the read acknowledge be sent for. + This has priority over the ``message`` parameter. + + clear_mentions (`bool`): + Whether the mention badge should be cleared (so that + there are no more mentions) or not for the given entity. + + If no message is provided, this will be the only action + taken. + + Example + .. code-block:: python + + # using a Message object + await client.send_read_acknowledge(chat, message) + # ...or using the int ID of a Message + await client.send_read_acknowledge(chat, message_id) + # ...or passing a list of messages to mark as read + await client.send_read_acknowledge(chat, messages) + """ + return messages.send_read_acknowledge(**locals()) + + async def pin_message( + self: 'TelegramClient', + entity: 'hints.EntityLike', + message: 'typing.Optional[hints.MessageIDLike]', + *, + notify: bool = False, + pm_oneside: bool = False + ): + """ + Pins a message in a chat. + + The default behaviour is to *not* notify members, unlike the + official applications. + + See also `Message.pin() `. + + Arguments + entity (`entity`): + The chat where the message should be pinned. + + message (`int` | `Message `): + The message or the message ID to pin. If it's + `None`, all messages will be unpinned instead. + + notify (`bool`, optional): + Whether the pin should notify people or not. + + pm_oneside (`bool`, optional): + Whether the message should be pinned for everyone or not. + By default it has the opposite behaviour of official clients, + and it will pin the message for both sides, in private chats. + + Example + .. code-block:: python + + # Send and pin a message to annoy everyone + message = await client.send_message(chat, 'Pinotifying is fun!') + await client.pin_message(chat, message, notify=True) + """ + return messages.pin_message(**locals()) + + async def unpin_message( + self: 'TelegramClient', + entity: 'hints.EntityLike', + message: 'typing.Optional[hints.MessageIDLike]' = None, + *, + notify: bool = False + ): + """ + Unpins a message in a chat. + + If no message ID is specified, all pinned messages will be unpinned. + + See also `Message.unpin() `. + + Arguments + entity (`entity`): + The chat where the message should be pinned. + + message (`int` | `Message `): + The message or the message ID to unpin. If it's + `None`, all messages will be unpinned instead. + + Example + .. code-block:: python + + # Unpin all messages from a chat + await client.unpin_message(chat) + """ + return messages.unpin_message(**locals()) + + # endregion Messages + + # region Base + + # Current TelegramClient version + __version__ = version.__version__ + + # Cached server configuration (with .dc_options), can be "global" + _config = None + _cdn_config = None + + def __init__( + self: 'TelegramClient', + session: 'typing.Union[str, Session]', + api_id: int, + api_hash: str, + *, + connection: 'typing.Type[Connection]' = ConnectionTcpFull, + use_ipv6: bool = False, + proxy: typing.Union[tuple, dict] = None, + local_addr: typing.Union[str, tuple] = None, + timeout: int = 10, + request_retries: int = 5, + connection_retries: int = 5, + retry_delay: int = 1, + auto_reconnect: bool = True, + sequential_updates: bool = False, + flood_sleep_threshold: int = 60, + raise_last_call_error: bool = False, + device_model: str = None, + system_version: str = None, + app_version: str = None, + lang_code: str = 'en', + system_lang_code: str = 'en', + loop: asyncio.AbstractEventLoop = None, + base_logger: typing.Union[str, logging.Logger] = None, + receive_updates: bool = True + ): + return telegrambaseclient.init(**locals()) + + @property + def loop(self: 'TelegramClient') -> asyncio.AbstractEventLoop: + """ + Property with the ``asyncio`` event loop used by this client. + + Example + .. code-block:: python + + # Download media in the background + task = client.loop.create_task(message.download_media()) + + # Do some work + ... + + # Join the task (wait for it to complete) + await task + """ + return telegrambaseclient.get_loop(**locals()) + + @property + def disconnected(self: 'TelegramClient') -> asyncio.Future: + """ + Property with a ``Future`` that resolves upon disconnection. + + Example + .. code-block:: python + + # Wait for a disconnection to occur + try: + await client.disconnected + except OSError: + print('Error on disconnect') + """ + return telegrambaseclient.get_disconnected(**locals()) + + @property + def flood_sleep_threshold(self): + return telegrambaseclient.get_flood_sleep_threshold(**locals()) + + @flood_sleep_threshold.setter + def flood_sleep_threshold(self, value): + return telegrambaseclient.set_flood_sleep_threshold(**locals()) + + async def connect(self: 'TelegramClient') -> None: + """ + Connects to Telegram. + + .. note:: + + Connect means connect and nothing else, and only one low-level + request is made to notify Telegram about which layer we will be + using. + + Before Telegram sends you updates, you need to make a high-level + request, like `client.get_me() `, + as described in https://core.telegram.org/api/updates. + + Example + .. code-block:: python + + try: + await client.connect() + except OSError: + print('Failed to connect') + """ + return telegrambaseclient.connect(**locals()) + + def is_connected(self: 'TelegramClient') -> bool: + """ + Returns `True` if the user has connected. + + This method is **not** asynchronous (don't use ``await`` on it). + + Example + .. code-block:: python + + while client.is_connected(): + await asyncio.sleep(1) + """ + return telegrambaseclient.is_connected(**locals()) + + def disconnect(self: 'TelegramClient'): + """ + Disconnects from Telegram. + + If the event loop is already running, this method returns a + coroutine that you should await on your own code; otherwise + the loop is ran until said coroutine completes. + + Example + .. code-block:: python + + # You don't need to use this if you used "with client" + await client.disconnect() + """ + return telegrambaseclient.disconnect(**locals()) + + def set_proxy(self: 'TelegramClient', proxy: typing.Union[tuple, dict]): + """ + Changes the proxy which will be used on next (re)connection. + + Method has no immediate effects if the client is currently connected. + + The new proxy will take it's effect on the next reconnection attempt: + - on a call `await client.connect()` (after complete disconnect) + - on auto-reconnect attempt (e.g, after previous connection was lost) + """ + return telegrambaseclient.set_proxy(**locals()) + + # endregion Base + + # region Updates + + async def set_receive_updates(self: 'TelegramClient', receive_updates): + """ + Change the value of `receive_updates`. + + This is an `async` method, because in order for Telegram to start + sending updates again, a request must be made. + """ + return updates.set_receive_updates(**locals()) + + def run_until_disconnected(self: 'TelegramClient'): + """ + Runs the event loop until the library is disconnected. + + It also notifies Telegram that we want to receive updates + as described in https://core.telegram.org/api/updates. + + Manual disconnections can be made by calling `disconnect() + ` + or sending a ``KeyboardInterrupt`` (e.g. by pressing ``Ctrl+C`` on + the console window running the script). + + If a disconnection error occurs (i.e. the library fails to reconnect + automatically), said error will be raised through here, so you have a + chance to ``except`` it on your own code. + + If the loop is already running, this method returns a coroutine + that you should await on your own code. + + .. note:: + + If you want to handle ``KeyboardInterrupt`` in your code, + simply run the event loop in your code too in any way, such as + ``loop.run_forever()`` or ``await client.disconnected`` (e.g. + ``loop.run_until_complete(client.disconnected)``). + + Example + .. code-block:: python + + # Blocks the current task here until a disconnection occurs. + # + # You will still receive updates, since this prevents the + # script from exiting. + await client.run_until_disconnected() + """ + return updates.run_until_disconnected(**locals()) + + def on(self: 'TelegramClient', event: EventBuilder): + """ + Decorator used to `add_event_handler` more conveniently. + + + Arguments + event (`_EventBuilder` | `type`): + The event builder class or instance to be used, + for instance ``events.NewMessage``. + + Example + .. code-block:: python + + from telethon import TelegramClient, events + client = TelegramClient(...) + + # Here we use client.on + @client.on(events.NewMessage) + async def handler(event): + ... + """ + return updates.on(**locals()) + + def add_event_handler( + self: 'TelegramClient', + callback: Callback, + event: EventBuilder = None): + """ + Registers a new event handler callback. + + The callback will be called when the specified event occurs. + + Arguments + callback (`callable`): + The callable function accepting one parameter to be used. + + Note that if you have used `telethon.events.register` in + the callback, ``event`` will be ignored, and instead the + events you previously registered will be used. + + event (`_EventBuilder` | `type`, optional): + The event builder class or instance to be used, + for instance ``events.NewMessage``. + + If left unspecified, `telethon.events.raw.Raw` (the + :tl:`Update` objects with no further processing) will + be passed instead. + + Example + .. code-block:: python + + from telethon import TelegramClient, events + client = TelegramClient(...) + + async def handler(event): + ... + + client.add_event_handler(handler, events.NewMessage) + """ + return updates.add_event_handler(**locals()) + + def remove_event_handler( + self: 'TelegramClient', + callback: Callback, + event: EventBuilder = None) -> int: + """ + Inverse operation of `add_event_handler()`. + + If no event is given, all events for this callback are removed. + Returns how many callbacks were removed. + + Example + .. code-block:: python + + @client.on(events.Raw) + @client.on(events.NewMessage) + async def handler(event): + ... + + # Removes only the "Raw" handling + # "handler" will still receive "events.NewMessage" + client.remove_event_handler(handler, events.Raw) + + # "handler" will stop receiving anything + client.remove_event_handler(handler) + """ + return updates.remove_event_handler(**locals()) + + def list_event_handlers(self: 'TelegramClient')\ + -> 'typing.Sequence[typing.Tuple[Callback, EventBuilder]]': + """ + Lists all registered event handlers. + + Returns + A list of pairs consisting of ``(callback, event)``. + + Example + .. code-block:: python + + @client.on(events.NewMessage(pattern='hello')) + async def on_greeting(event): + '''Greets someone''' + await event.reply('Hi') + + for callback, event in client.list_event_handlers(): + print(id(callback), type(event)) + """ + return updates.list_event_handlers(**locals()) + + async def catch_up(self: 'TelegramClient'): + """ + "Catches up" on the missed updates while the client was offline. + You should call this method after registering the event handlers + so that the updates it loads can by processed by your script. + + This can also be used to forcibly fetch new updates if there are any. + + Example + .. code-block:: python + + await client.catch_up() + """ + return updates.catch_up(**locals()) + + # endregion Updates + + # region Uploads + + async def send_file( + self: 'TelegramClient', + entity: 'hints.EntityLike', + file: 'typing.Union[hints.FileLike, typing.Sequence[hints.FileLike]]', + *, + caption: typing.Union[str, typing.Sequence[str]] = None, + force_document: bool = False, + file_size: int = None, + clear_draft: bool = False, + progress_callback: 'hints.ProgressCallback' = None, + reply_to: 'hints.MessageIDLike' = None, + attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None, + thumb: 'hints.FileLike' = None, + allow_cache: bool = True, + parse_mode: str = (), + formatting_entities: typing.Optional[typing.List[types.TypeMessageEntity]] = None, + voice_note: bool = False, + video_note: bool = False, + buttons: 'hints.MarkupLike' = None, + silent: bool = None, + background: bool = None, + supports_streaming: bool = False, + schedule: 'hints.DateLike' = None, + comment_to: 'typing.Union[int, types.Message]' = None, + ttl: int = None, + **kwargs) -> 'types.Message': + """ + Sends message with the given file to the specified entity. + + .. note:: + + If the ``hachoir3`` package (``hachoir`` module) is installed, + it will be used to determine metadata from audio and video files. + + If the ``pillow`` package is installed and you are sending a photo, + it will be resized to fit within the maximum dimensions allowed + by Telegram to avoid ``errors.PhotoInvalidDimensionsError``. This + cannot be done if you are sending :tl:`InputFile`, however. + + Arguments + entity (`entity`): + Who will receive the file. + + file (`str` | `bytes` | `file` | `media`): + The file to send, which can be one of: + + * A local file path to an in-disk file. The file name + will be the path's base name. + + * A `bytes` byte array with the file's data to send + (for example, by using ``text.encode('utf-8')``). + A default file name will be used. + + * A bytes `io.IOBase` stream over the file to send + (for example, by using ``open(file, 'rb')``). + Its ``.name`` property will be used for the file name, + or a default if it doesn't have one. + + * An external URL to a file over the internet. This will + send the file as "external" media, and Telegram is the + one that will fetch the media and send it. + + * A Bot API-like ``file_id``. You can convert previously + sent media to file IDs for later reusing with + `telethon.utils.pack_bot_file_id`. + + * A handle to an existing file (for example, if you sent a + message with media before, you can use its ``message.media`` + as a file here). + + * A handle to an uploaded file (from `upload_file`). + + * A :tl:`InputMedia` instance. For example, if you want to + send a dice use :tl:`InputMediaDice`, or if you want to + send a contact use :tl:`InputMediaContact`. + + To send an album, you should provide a list in this parameter. + + If a list or similar is provided, the files in it will be + sent as an album in the order in which they appear, sliced + in chunks of 10 if more than 10 are given. + + caption (`str`, optional): + Optional caption for the sent media message. When sending an + album, the caption may be a list of strings, which will be + assigned to the files pairwise. + + force_document (`bool`, optional): + If left to `False` and the file is a path that ends with + the extension of an image file or a video file, it will be + sent as such. Otherwise always as a document. + + file_size (`int`, optional): + The size of the file to be uploaded if it needs to be uploaded, + which will be determined automatically if not specified. + + If the file size can't be determined beforehand, the entire + file will be read in-memory to find out how large it is. + + clear_draft (`bool`, optional): + Whether the existing draft should be cleared or not. + + progress_callback (`callable`, optional): + A callback function accepting two parameters: + ``(sent bytes, total)``. + + reply_to (`int` | `Message `): + Same as `reply_to` from `send_message`. + + attributes (`list`, optional): + Optional attributes that override the inferred ones, like + :tl:`DocumentAttributeFilename` and so on. + + thumb (`str` | `bytes` | `file`, optional): + Optional JPEG thumbnail (for documents). **Telegram will + ignore this parameter** unless you pass a ``.jpg`` file! + + The file must also be small in dimensions and in disk size. + Successful thumbnails were files below 20kB and 320x320px. + Width/height and dimensions/size ratios may be important. + For Telegram to accept a thumbnail, you must provide the + dimensions of the underlying media through ``attributes=`` + with :tl:`DocumentAttributesVideo` or by installing the + optional ``hachoir`` dependency. + + + allow_cache (`bool`, optional): + This parameter currently does nothing, but is kept for + backward-compatibility (and it may get its use back in + the future). + + parse_mode (`object`, optional): + See the `TelegramClient.parse_mode + ` + property for allowed values. Markdown parsing will be used by + default. + + formatting_entities (`list`, optional): + A list of message formatting entities. When provided, the ``parse_mode`` is ignored. + + voice_note (`bool`, optional): + If `True` the audio will be sent as a voice note. + + video_note (`bool`, optional): + If `True` the video will be sent as a video note, + also known as a round video message. + + buttons (`list`, `custom.Button `, :tl:`KeyboardButton`): + The matrix (list of lists), row list or button to be shown + after sending the message. This parameter will only work if + you have signed in as a bot. You can also pass your own + :tl:`ReplyMarkup` here. + + silent (`bool`, optional): + Whether the message should notify people with sound or not. + Defaults to `False` (send with a notification sound unless + the person has the chat muted). Set it to `True` to alter + this behaviour. + + background (`bool`, optional): + Whether the message should be send in background. + + supports_streaming (`bool`, optional): + Whether the sent video supports streaming or not. Note that + Telegram only recognizes as streamable some formats like MP4, + and others like AVI or MKV will not work. You should convert + these to MP4 before sending if you want them to be streamable. + Unsupported formats will result in ``VideoContentTypeError``. + + schedule (`hints.DateLike`, optional): + If set, the file won't send immediately, and instead + it will be scheduled to be automatically sent at a later + time. + + comment_to (`int` | `Message `, optional): + Similar to ``reply_to``, but replies in the linked group of a + broadcast channel instead (effectively leaving a "comment to" + the specified message). + + This parameter takes precedence over ``reply_to``. If there is + no linked chat, `telethon.errors.sgIdInvalidError` is raised. + + ttl (`int`. optional): + The Time-To-Live of the file (also known as "self-destruct timer" + or "self-destructing media"). If set, files can only be viewed for + a short period of time before they disappear from the message + history automatically. + + The value must be at least 1 second, and at most 60 seconds, + otherwise Telegram will ignore this parameter. + + Not all types of media can be used with this parameter, such + as text documents, which will fail with ``TtlMediaInvalidError``. + + Returns + The `Message ` (or messages) + containing the sent file, or messages if a list of them was passed. + + Example + .. code-block:: python + + # Normal files like photos + await client.send_file(chat, '/my/photos/me.jpg', caption="It's me!") + # or + await client.send_message(chat, "It's me!", file='/my/photos/me.jpg') + + # Voice notes or round videos + await client.send_file(chat, '/my/songs/song.mp3', voice_note=True) + await client.send_file(chat, '/my/videos/video.mp4', video_note=True) + + # Custom thumbnails + await client.send_file(chat, '/my/documents/doc.txt', thumb='photo.jpg') + + # Only documents + await client.send_file(chat, '/my/photos/photo.png', force_document=True) + + # Albums + await client.send_file(chat, [ + '/my/photos/holiday1.jpg', + '/my/photos/holiday2.jpg', + '/my/drawings/portrait.png' + ]) + + # Printing upload progress + def callback(current, total): + print('Uploaded', current, 'out of', total, + 'bytes: {:.2%}'.format(current / total)) + + await client.send_file(chat, file, progress_callback=callback) + + # Dices, including dart and other future emoji + from telethon.tl import types + await client.send_file(chat, types.InputMediaDice('')) + await client.send_file(chat, types.InputMediaDice('🎯')) + + # Contacts + await client.send_file(chat, types.InputMediaContact( + phone_number='+34 123 456 789', + first_name='Example', + last_name='', + vcard='' + )) + """ + return uploads.send_file(**locals()) + + async def upload_file( + self: 'TelegramClient', + file: 'hints.FileLike', + *, + part_size_kb: float = None, + file_size: int = None, + file_name: str = None, + use_cache: type = None, + key: bytes = None, + iv: bytes = None, + progress_callback: 'hints.ProgressCallback' = None) -> 'types.TypeInputFile': + """ + Uploads a file to Telegram's servers, without sending it. + + .. note:: + + Generally, you want to use `send_file` instead. + + This method returns a handle (an instance of :tl:`InputFile` or + :tl:`InputFileBig`, as required) which can be later used before + it expires (they are usable during less than a day). + + Uploading a file will simply return a "handle" to the file stored + remotely in the Telegram servers, which can be later used on. This + will **not** upload the file to your own chat or any chat at all. + + Arguments + file (`str` | `bytes` | `file`): + The path of the file, byte array, or stream that will be sent. + Note that if a byte array or a stream is given, a filename + or its type won't be inferred, and it will be sent as an + "unnamed application/octet-stream". + + part_size_kb (`int`, optional): + Chunk size when uploading files. The larger, the less + requests will be made (up to 512KB maximum). + + file_size (`int`, optional): + The size of the file to be uploaded, which will be determined + automatically if not specified. + + If the file size can't be determined beforehand, the entire + file will be read in-memory to find out how large it is. + + file_name (`str`, optional): + The file name which will be used on the resulting InputFile. + If not specified, the name will be taken from the ``file`` + and if this is not a `str`, it will be ``"unnamed"``. + + use_cache (`type`, optional): + This parameter currently does nothing, but is kept for + backward-compatibility (and it may get its use back in + the future). + + key ('bytes', optional): + In case of an encrypted upload (secret chats) a key is supplied + + iv ('bytes', optional): + In case of an encrypted upload (secret chats) an iv is supplied + + progress_callback (`callable`, optional): + A callback function accepting two parameters: + ``(sent bytes, total)``. + + Returns + :tl:`InputFileBig` if the file size is larger than 10MB, + `InputSizedFile ` + (subclass of :tl:`InputFile`) otherwise. + + Example + .. code-block:: python + + # Photos as photo and document + file = await client.upload_file('photo.jpg') + await client.send_file(chat, file) # sends as photo + await client.send_file(chat, file, force_document=True) # sends as document + + file.name = 'not a photo.jpg' + await client.send_file(chat, file, force_document=True) # document, new name + + # As song or as voice note + file = await client.upload_file('song.ogg') + await client.send_file(chat, file) # sends as song + await client.send_file(chat, file, voice_note=True) # sends as voice note + """ + return uploads.upload_file(**locals()) + + # endregion Uploads + + # region Users + + def __call__(self: 'TelegramClient', request, ordered=False): + """ + Invokes (sends) one or more MTProtoRequests and returns (receives) + their result. + + Args: + request (`TLObject` | `list`): + The request or requests to be invoked. + + ordered (`bool`, optional): + Whether the requests (if more than one was given) should be + executed sequentially on the server. They run in arbitrary + order by default. + + flood_sleep_threshold (`int` | `None`, optional): + The flood sleep threshold to use for this request. This overrides + the default value stored in + `client.flood_sleep_threshold ` + + Returns: + The result of the request (often a `TLObject`) or a list of + results if more than one request was given. + """ + return users.call(self._sender, request, ordered=ordered) + + async def get_me(self: 'TelegramClient', input_peer: bool = False) \ + -> 'typing.Union[types.User, types.InputPeerUser]': + """ + Gets "me", the current :tl:`User` who is logged in. + + If the user has not logged in yet, this method returns `None`. + + Arguments + input_peer (`bool`, optional): + Whether to return the :tl:`InputPeerUser` version or the normal + :tl:`User`. This can be useful if you just need to know the ID + of yourself. + + Returns + Your own :tl:`User`. + + Example + .. code-block:: python + + me = await client.get_me() + print(me.username) + """ + return users.get_me(**locals()) + + async def is_bot(self: 'TelegramClient') -> bool: + """ + Return `True` if the signed-in user is a bot, `False` otherwise. + + Example + .. code-block:: python + + if await client.is_bot(): + print('Beep') + else: + print('Hello') + """ + return users.is_bot(**locals()) + + async def is_user_authorized(self: 'TelegramClient') -> bool: + """ + Returns `True` if the user is authorized (logged in). + + Example + .. code-block:: python + + if not await client.is_user_authorized(): + await client.send_code_request(phone) + code = input('enter code: ') + await client.sign_in(phone, code) + """ + return users.is_user_authorized(**locals()) + + async def get_entity( + self: 'TelegramClient', + entity: 'hints.EntitiesLike') -> 'hints.Entity': + """ + Turns the given entity into a valid Telegram :tl:`User`, :tl:`Chat` + or :tl:`Channel`. You can also pass a list or iterable of entities, + and they will be efficiently fetched from the network. + + Arguments + entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`): + If a username is given, **the username will be resolved** making + an API call every time. Resolving usernames is an expensive + operation and will start hitting flood waits around 50 usernames + in a short period of time. + + If you want to get the entity for a *cached* username, you should + first `get_input_entity(username) ` which will + use the cache), and then use `get_entity` with the result of the + previous call. + + Similar limits apply to invite links, and you should use their + ID instead. + + Using phone numbers (from people in your contact list), exact + names, integer IDs or :tl:`Peer` rely on a `get_input_entity` + first, which in turn needs the entity to be in cache, unless + a :tl:`InputPeer` was passed. + + Unsupported types will raise ``TypeError``. + + If the entity can't be found, ``ValueError`` will be raised. + + Returns + :tl:`User`, :tl:`Chat` or :tl:`Channel` corresponding to the + input entity. A list will be returned if more than one was given. + + Example + .. code-block:: python + + from telethon import utils + + me = await client.get_entity('me') + print(utils.get_display_name(me)) + + chat = await client.get_input_entity('username') + async for message in client.iter_messages(chat): + ... + + # Note that you could have used the username directly, but it's + # good to use get_input_entity if you will reuse it a lot. + async for message in client.iter_messages('username'): + ... + + # Note that for this to work the phone number must be in your contacts + some_id = await client.get_peer_id('+34123456789') + """ + return users.get_entity(**locals()) + + async def get_input_entity( + self: 'TelegramClient', + peer: 'hints.EntityLike') -> 'types.TypeInputPeer': + """ + Turns the given entity into its input entity version. + + Most requests use this kind of :tl:`InputPeer`, so this is the most + suitable call to make for those cases. **Generally you should let the + library do its job** and don't worry about getting the input entity + first, but if you're going to use an entity often, consider making the + call: + + Arguments + entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`): + If a username or invite link is given, **the library will + use the cache**. This means that it's possible to be using + a username that *changed* or an old invite link (this only + happens if an invite link for a small group chat is used + after it was upgraded to a mega-group). + + If the username or ID from the invite link is not found in + the cache, it will be fetched. The same rules apply to phone + numbers (``'+34 123456789'``) from people in your contact list. + + If an exact name is given, it must be in the cache too. This + is not reliable as different people can share the same name + and which entity is returned is arbitrary, and should be used + only for quick tests. + + If a positive integer ID is given, the entity will be searched + in cached users, chats or channels, without making any call. + + If a negative integer ID is given, the entity will be searched + exactly as either a chat (prefixed with ``-``) or as a channel + (prefixed with ``-100``). + + If a :tl:`Peer` is given, it will be searched exactly in the + cache as either a user, chat or channel. + + If the given object can be turned into an input entity directly, + said operation will be done. + + Unsupported types will raise ``TypeError``. + + If the entity can't be found, ``ValueError`` will be raised. + + Returns + :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. + + Example + .. code-block:: python + + # If you're going to use "username" often in your code + # (make a lot of calls), consider getting its input entity + # once, and then using the "user" everywhere instead. + user = await client.get_input_entity('username') + + # The same applies to IDs, chats or channels. + chat = await client.get_input_entity(-123456789) + """ + return users.get_input_entity(**locals()) + + async def get_peer_id( + self: 'TelegramClient', + peer: 'hints.EntityLike', + add_mark: bool = True) -> int: + """ + Gets the ID for the given entity. + + This method needs to be ``async`` because `peer` supports usernames, + invite-links, phone numbers (from people in your contact list), etc. + + If ``add_mark is False``, then a positive ID will be returned + instead. By default, bot-API style IDs (signed) are returned. + + Example + .. code-block:: python + + print(await client.get_peer_id('me')) + """ + return users.get_peer_id(**locals()) + + # endregion Users + +# TODO re-patch everything to remove the intermediate calls diff --git a/telethon/client/updates.py b/telethon/client/updates.py index bcc983f3..4860a8cd 100644 --- a/telethon/client/updates.py +++ b/telethon/client/updates.py @@ -18,616 +18,608 @@ if typing.TYPE_CHECKING: Callback = typing.Callable[[typing.Any], typing.Any] -class UpdateMethods: - # region Public methods +async def _run_until_disconnected(self: 'TelegramClient'): + try: + # Make a high-level request to notify that we want updates + await self(functions.updates.GetStateRequest()) + return await self.disconnected + except KeyboardInterrupt: + pass + finally: + await self.disconnect() - async def _run_until_disconnected(self: 'TelegramClient'): - try: - # Make a high-level request to notify that we want updates - await self(functions.updates.GetStateRequest()) - return await self.disconnected - except KeyboardInterrupt: - pass - finally: - await self.disconnect() +async def set_receive_updates(self: 'TelegramClient', receive_updates): + """ + Change the value of `receive_updates`. - async def set_receive_updates(self: 'TelegramClient', receive_updates): - """ - Change the value of `receive_updates`. + This is an `async` method, because in order for Telegram to start + sending updates again, a request must be made. + """ + self._no_updates = not receive_updates + if receive_updates: + await self(functions.updates.GetStateRequest()) - This is an `async` method, because in order for Telegram to start - sending updates again, a request must be made. - """ - self._no_updates = not receive_updates - if receive_updates: - await self(functions.updates.GetStateRequest()) +def run_until_disconnected(self: 'TelegramClient'): + """ + Runs the event loop until the library is disconnected. - def run_until_disconnected(self: 'TelegramClient'): - """ - Runs the event loop until the library is disconnected. + It also notifies Telegram that we want to receive updates + as described in https://core.telegram.org/api/updates. - It also notifies Telegram that we want to receive updates - as described in https://core.telegram.org/api/updates. + Manual disconnections can be made by calling `disconnect() + ` + or sending a ``KeyboardInterrupt`` (e.g. by pressing ``Ctrl+C`` on + the console window running the script). - Manual disconnections can be made by calling `disconnect() - ` - or sending a ``KeyboardInterrupt`` (e.g. by pressing ``Ctrl+C`` on - the console window running the script). + If a disconnection error occurs (i.e. the library fails to reconnect + automatically), said error will be raised through here, so you have a + chance to ``except`` it on your own code. - If a disconnection error occurs (i.e. the library fails to reconnect - automatically), said error will be raised through here, so you have a - chance to ``except`` it on your own code. + If the loop is already running, this method returns a coroutine + that you should await on your own code. - If the loop is already running, this method returns a coroutine - that you should await on your own code. + .. note:: - .. note:: + If you want to handle ``KeyboardInterrupt`` in your code, + simply run the event loop in your code too in any way, such as + ``loop.run_forever()`` or ``await client.disconnected`` (e.g. + ``loop.run_until_complete(client.disconnected)``). - If you want to handle ``KeyboardInterrupt`` in your code, - simply run the event loop in your code too in any way, such as - ``loop.run_forever()`` or ``await client.disconnected`` (e.g. - ``loop.run_until_complete(client.disconnected)``). + Example + .. code-block:: python - Example - .. code-block:: python + # Blocks the current task here until a disconnection occurs. + # + # You will still receive updates, since this prevents the + # script from exiting. + await client.run_until_disconnected() + """ + if self.loop.is_running(): + return self._run_until_disconnected() + try: + return self.loop.run_until_complete(self._run_until_disconnected()) + except KeyboardInterrupt: + pass + finally: + # No loop.run_until_complete; it's already syncified + self.disconnect() - # Blocks the current task here until a disconnection occurs. - # - # You will still receive updates, since this prevents the - # script from exiting. - await client.run_until_disconnected() - """ - if self.loop.is_running(): - return self._run_until_disconnected() - try: - return self.loop.run_until_complete(self._run_until_disconnected()) - except KeyboardInterrupt: - pass - finally: - # No loop.run_until_complete; it's already syncified - self.disconnect() - - def on(self: 'TelegramClient', event: EventBuilder): - """ - Decorator used to `add_event_handler` more conveniently. +def on(self: 'TelegramClient', event: EventBuilder): + """ + Decorator used to `add_event_handler` more conveniently. - Arguments - event (`_EventBuilder` | `type`): - The event builder class or instance to be used, - for instance ``events.NewMessage``. + Arguments + event (`_EventBuilder` | `type`): + The event builder class or instance to be used, + for instance ``events.NewMessage``. - Example - .. code-block:: python + Example + .. code-block:: python - from telethon import TelegramClient, events - client = TelegramClient(...) + from telethon import TelegramClient, events + client = TelegramClient(...) - # Here we use client.on - @client.on(events.NewMessage) - async def handler(event): - ... - """ - def decorator(f): - self.add_event_handler(f, event) - return f + # Here we use client.on + @client.on(events.NewMessage) + async def handler(event): + ... + """ + def decorator(f): + self.add_event_handler(f, event) + return f - return decorator + return decorator - def add_event_handler( - self: 'TelegramClient', - callback: Callback, - event: EventBuilder = None): - """ - Registers a new event handler callback. +def add_event_handler( + self: 'TelegramClient', + callback: Callback, + event: EventBuilder = None): + """ + Registers a new event handler callback. - The callback will be called when the specified event occurs. + The callback will be called when the specified event occurs. - Arguments - callback (`callable`): - The callable function accepting one parameter to be used. + Arguments + callback (`callable`): + The callable function accepting one parameter to be used. - Note that if you have used `telethon.events.register` in - the callback, ``event`` will be ignored, and instead the - events you previously registered will be used. + Note that if you have used `telethon.events.register` in + the callback, ``event`` will be ignored, and instead the + events you previously registered will be used. - event (`_EventBuilder` | `type`, optional): - The event builder class or instance to be used, - for instance ``events.NewMessage``. + event (`_EventBuilder` | `type`, optional): + The event builder class or instance to be used, + for instance ``events.NewMessage``. - If left unspecified, `telethon.events.raw.Raw` (the - :tl:`Update` objects with no further processing) will - be passed instead. + If left unspecified, `telethon.events.raw.Raw` (the + :tl:`Update` objects with no further processing) will + be passed instead. - Example - .. code-block:: python + Example + .. code-block:: python - from telethon import TelegramClient, events - client = TelegramClient(...) + from telethon import TelegramClient, events + client = TelegramClient(...) - async def handler(event): - ... + async def handler(event): + ... - client.add_event_handler(handler, events.NewMessage) - """ - builders = events._get_handlers(callback) - if builders is not None: - for event in builders: - self._event_builders.append((event, callback)) - return + client.add_event_handler(handler, events.NewMessage) + """ + builders = events._get_handlers(callback) + if builders is not None: + for event in builders: + self._event_builders.append((event, callback)) + return - if isinstance(event, type): - event = event() - elif not event: - event = events.Raw() + if isinstance(event, type): + event = event() + elif not event: + event = events.Raw() - self._event_builders.append((event, callback)) + self._event_builders.append((event, callback)) - def remove_event_handler( - self: 'TelegramClient', - callback: Callback, - event: EventBuilder = None) -> int: - """ - Inverse operation of `add_event_handler()`. +def remove_event_handler( + self: 'TelegramClient', + callback: Callback, + event: EventBuilder = None) -> int: + """ + Inverse operation of `add_event_handler()`. - If no event is given, all events for this callback are removed. - Returns how many callbacks were removed. + If no event is given, all events for this callback are removed. + Returns how many callbacks were removed. - Example - .. code-block:: python + Example + .. code-block:: python - @client.on(events.Raw) - @client.on(events.NewMessage) - async def handler(event): - ... + @client.on(events.Raw) + @client.on(events.NewMessage) + async def handler(event): + ... - # Removes only the "Raw" handling - # "handler" will still receive "events.NewMessage" - client.remove_event_handler(handler, events.Raw) + # Removes only the "Raw" handling + # "handler" will still receive "events.NewMessage" + client.remove_event_handler(handler, events.Raw) - # "handler" will stop receiving anything - client.remove_event_handler(handler) - """ - found = 0 - if event and not isinstance(event, type): - event = type(event) + # "handler" will stop receiving anything + client.remove_event_handler(handler) + """ + found = 0 + if event and not isinstance(event, type): + event = type(event) - i = len(self._event_builders) - while i: - i -= 1 - ev, cb = self._event_builders[i] - if cb == callback and (not event or isinstance(ev, event)): - del self._event_builders[i] - found += 1 + i = len(self._event_builders) + while i: + i -= 1 + ev, cb = self._event_builders[i] + if cb == callback and (not event or isinstance(ev, event)): + del self._event_builders[i] + found += 1 - return found + return found - def list_event_handlers(self: 'TelegramClient')\ - -> 'typing.Sequence[typing.Tuple[Callback, EventBuilder]]': - """ - Lists all registered event handlers. +def list_event_handlers(self: 'TelegramClient')\ + -> 'typing.Sequence[typing.Tuple[Callback, EventBuilder]]': + """ + Lists all registered event handlers. - Returns - A list of pairs consisting of ``(callback, event)``. + Returns + A list of pairs consisting of ``(callback, event)``. - Example - .. code-block:: python + Example + .. code-block:: python - @client.on(events.NewMessage(pattern='hello')) - async def on_greeting(event): - '''Greets someone''' - await event.reply('Hi') + @client.on(events.NewMessage(pattern='hello')) + async def on_greeting(event): + '''Greets someone''' + await event.reply('Hi') - for callback, event in client.list_event_handlers(): - print(id(callback), type(event)) - """ - return [(callback, event) for event, callback in self._event_builders] + for callback, event in client.list_event_handlers(): + print(id(callback), type(event)) + """ + return [(callback, event) for event, callback in self._event_builders] - async def catch_up(self: 'TelegramClient'): - """ - "Catches up" on the missed updates while the client was offline. - You should call this method after registering the event handlers - so that the updates it loads can by processed by your script. +async def catch_up(self: 'TelegramClient'): + """ + "Catches up" on the missed updates while the client was offline. + You should call this method after registering the event handlers + so that the updates it loads can by processed by your script. - This can also be used to forcibly fetch new updates if there are any. + This can also be used to forcibly fetch new updates if there are any. - Example - .. code-block:: python + Example + .. code-block:: python - await client.catch_up() - """ - pts, date = self._state_cache[None] - if not pts: - return + await client.catch_up() + """ + pts, date = self._state_cache[None] + if not pts: + return - self.session.catching_up = True - try: - while True: - d = await self(functions.updates.GetDifferenceRequest( - pts, date, 0 - )) - if isinstance(d, (types.updates.DifferenceSlice, - types.updates.Difference)): - if isinstance(d, types.updates.Difference): - state = d.state - else: - state = d.intermediate_state - - pts, date = state.pts, state.date - self._handle_update(types.Updates( - users=d.users, - chats=d.chats, - date=state.date, - seq=state.seq, - updates=d.other_updates + [ - types.UpdateNewMessage(m, 0, 0) - for m in d.new_messages - ] - )) - - # TODO Implement upper limit (max_pts) - # We don't want to fetch updates we already know about. - # - # We may still get duplicates because the Difference - # contains a lot of updates and presumably only has - # the state for the last one, but at least we don't - # unnecessarily fetch too many. - # - # updates.getDifference's pts_total_limit seems to mean - # "how many pts is the request allowed to return", and - # if there is more than that, it returns "too long" (so - # there would be duplicate updates since we know about - # some). This can be used to detect collisions (i.e. - # it would return an update we have already seen). + self.session.catching_up = True + try: + while True: + d = await self(functions.updates.GetDifferenceRequest( + pts, date, 0 + )) + if isinstance(d, (types.updates.DifferenceSlice, + types.updates.Difference)): + if isinstance(d, types.updates.Difference): + state = d.state else: - if isinstance(d, types.updates.DifferenceEmpty): - date = d.date - elif isinstance(d, types.updates.DifferenceTooLong): - pts = d.pts - break - except (ConnectionError, asyncio.CancelledError): + state = d.intermediate_state + + pts, date = state.pts, state.date + self._handle_update(types.Updates( + users=d.users, + chats=d.chats, + date=state.date, + seq=state.seq, + updates=d.other_updates + [ + types.UpdateNewMessage(m, 0, 0) + for m in d.new_messages + ] + )) + + # TODO Implement upper limit (max_pts) + # We don't want to fetch updates we already know about. + # + # We may still get duplicates because the Difference + # contains a lot of updates and presumably only has + # the state for the last one, but at least we don't + # unnecessarily fetch too many. + # + # updates.getDifference's pts_total_limit seems to mean + # "how many pts is the request allowed to return", and + # if there is more than that, it returns "too long" (so + # there would be duplicate updates since we know about + # some). This can be used to detect collisions (i.e. + # it would return an update we have already seen). + else: + if isinstance(d, types.updates.DifferenceEmpty): + date = d.date + elif isinstance(d, types.updates.DifferenceTooLong): + pts = d.pts + break + except (ConnectionError, asyncio.CancelledError): + pass + finally: + # TODO Save new pts to session + self._state_cache._pts_date = (pts, date) + self.session.catching_up = False + + +# It is important to not make _handle_update async because we rely on +# the order that the updates arrive in to update the pts and date to +# be always-increasing. There is also no need to make this async. +def _handle_update(self: 'TelegramClient', update): + self.session.process_entities(update) + self._entity_cache.add(update) + + if isinstance(update, (types.Updates, types.UpdatesCombined)): + entities = {utils.get_peer_id(x): x for x in + itertools.chain(update.users, update.chats)} + for u in update.updates: + self._process_update(u, update.updates, entities=entities) + elif isinstance(update, types.UpdateShort): + self._process_update(update.update, None) + else: + self._process_update(update, None) + + self._state_cache.update(update) + +def _process_update(self: 'TelegramClient', update, others, entities=None): + update._entities = entities or {} + + # This part is somewhat hot so we don't bother patching + # update with channel ID/its state. Instead we just pass + # arguments which is faster. + channel_id = self._state_cache.get_channel_id(update) + args = (update, others, channel_id, self._state_cache[channel_id]) + if self._dispatching_updates_queue is None: + task = self.loop.create_task(self._dispatch_update(*args)) + self._updates_queue.add(task) + task.add_done_callback(lambda _: self._updates_queue.discard(task)) + else: + self._updates_queue.put_nowait(args) + if not self._dispatching_updates_queue.is_set(): + self._dispatching_updates_queue.set() + self.loop.create_task(self._dispatch_queue_updates()) + + self._state_cache.update(update) + +async def _update_loop(self: 'TelegramClient'): + # Pings' ID don't really need to be secure, just "random" + rnd = lambda: random.randrange(-2**63, 2**63) + while self.is_connected(): + try: + await asyncio.wait_for( + self.disconnected, timeout=60 + ) + continue # We actually just want to act upon timeout + except asyncio.TimeoutError: pass - finally: - # TODO Save new pts to session - self._state_cache._pts_date = (pts, date) - self.session.catching_up = False + except asyncio.CancelledError: + return + except Exception: + continue # Any disconnected exception should be ignored - # endregion + # Check if we have any exported senders to clean-up periodically + await self._clean_exported_senders() - # region Private methods + # Don't bother sending pings until the low-level connection is + # ready, otherwise a lot of pings will be batched to be sent upon + # reconnect, when we really don't care about that. + if not self._sender._transport_connected(): + continue - # It is important to not make _handle_update async because we rely on - # the order that the updates arrive in to update the pts and date to - # be always-increasing. There is also no need to make this async. - def _handle_update(self: 'TelegramClient', update): - self.session.process_entities(update) - self._entity_cache.add(update) + # We also don't really care about their result. + # Just send them periodically. + try: + self._sender._keepalive_ping(rnd()) + except (ConnectionError, asyncio.CancelledError): + return - if isinstance(update, (types.Updates, types.UpdatesCombined)): - entities = {utils.get_peer_id(x): x for x in - itertools.chain(update.users, update.chats)} - for u in update.updates: - self._process_update(u, update.updates, entities=entities) - elif isinstance(update, types.UpdateShort): - self._process_update(update.update, None) - else: - self._process_update(update, None) + # Entities and cached files are not saved when they are + # inserted because this is a rather expensive operation + # (default's sqlite3 takes ~0.1s to commit changes). Do + # it every minute instead. No-op if there's nothing new. + self.session.save() - self._state_cache.update(update) - - def _process_update(self: 'TelegramClient', update, others, entities=None): - update._entities = entities or {} - - # This part is somewhat hot so we don't bother patching - # update with channel ID/its state. Instead we just pass - # arguments which is faster. - channel_id = self._state_cache.get_channel_id(update) - args = (update, others, channel_id, self._state_cache[channel_id]) - if self._dispatching_updates_queue is None: - task = self.loop.create_task(self._dispatch_update(*args)) - self._updates_queue.add(task) - task.add_done_callback(lambda _: self._updates_queue.discard(task)) - else: - self._updates_queue.put_nowait(args) - if not self._dispatching_updates_queue.is_set(): - self._dispatching_updates_queue.set() - self.loop.create_task(self._dispatch_queue_updates()) - - self._state_cache.update(update) - - async def _update_loop(self: 'TelegramClient'): - # Pings' ID don't really need to be secure, just "random" - rnd = lambda: random.randrange(-2**63, 2**63) - while self.is_connected(): - try: - await asyncio.wait_for( - self.disconnected, timeout=60 - ) - continue # We actually just want to act upon timeout - except asyncio.TimeoutError: - pass - except asyncio.CancelledError: - return - except Exception: - continue # Any disconnected exception should be ignored - - # Check if we have any exported senders to clean-up periodically - await self._clean_exported_senders() - - # Don't bother sending pings until the low-level connection is - # ready, otherwise a lot of pings will be batched to be sent upon - # reconnect, when we really don't care about that. - if not self._sender._transport_connected(): + # We need to send some content-related request at least hourly + # for Telegram to keep delivering updates, otherwise they will + # just stop even if we're connected. Do so every 30 minutes. + # + # TODO Call getDifference instead since it's more relevant + if time.time() - self._last_request > 30 * 60: + if not await self.is_user_authorized(): + # What can be the user doing for so + # long without being logged in...? continue - # We also don't really care about their result. - # Just send them periodically. try: - self._sender._keepalive_ping(rnd()) + await self(functions.updates.GetStateRequest()) except (ConnectionError, asyncio.CancelledError): return - # Entities and cached files are not saved when they are - # inserted because this is a rather expensive operation - # (default's sqlite3 takes ~0.1s to commit changes). Do - # it every minute instead. No-op if there's nothing new. - self.session.save() +async def _dispatch_queue_updates(self: 'TelegramClient'): + while not self._updates_queue.empty(): + await self._dispatch_update(*self._updates_queue.get_nowait()) - # We need to send some content-related request at least hourly - # for Telegram to keep delivering updates, otherwise they will - # just stop even if we're connected. Do so every 30 minutes. - # - # TODO Call getDifference instead since it's more relevant - if time.time() - self._last_request > 30 * 60: - if not await self.is_user_authorized(): - # What can be the user doing for so - # long without being logged in...? - continue + self._dispatching_updates_queue.clear() - try: - await self(functions.updates.GetStateRequest()) - except (ConnectionError, asyncio.CancelledError): - return - - async def _dispatch_queue_updates(self: 'TelegramClient'): - while not self._updates_queue.empty(): - await self._dispatch_update(*self._updates_queue.get_nowait()) - - self._dispatching_updates_queue.clear() - - async def _dispatch_update(self: 'TelegramClient', update, others, channel_id, pts_date): - if not self._entity_cache.ensure_cached(update): - # We could add a lock to not fetch the same pts twice if we are - # already fetching it. However this does not happen in practice, - # which makes sense, because different updates have different pts. - if self._state_cache.update(update, check_only=True): - # If the update doesn't have pts, fetching won't do anything. - # For example, UpdateUserStatus or UpdateChatUserTyping. - try: - await self._get_difference(update, channel_id, pts_date) - except OSError: - pass # We were disconnected, that's okay - except errors.RPCError: - # There's a high chance the request fails because we lack - # the channel. Because these "happen sporadically" (#1428) - # we should be okay (no flood waits) even if more occur. - pass - except ValueError: - # There is a chance that GetFullChannelRequest and GetDifferenceRequest - # inside the _get_difference() function will end up with - # ValueError("Request was unsuccessful N time(s)") for whatever reasons. - pass - - if not self._self_input_peer: - # Some updates require our own ID, so we must make sure - # that the event builder has offline access to it. Calling - # `get_me()` will cache it under `self._self_input_peer`. - # - # It will return `None` if we haven't logged in yet which is - # fine, we will just retry next time anyway. +async def _dispatch_update(self: 'TelegramClient', update, others, channel_id, pts_date): + if not self._entity_cache.ensure_cached(update): + # We could add a lock to not fetch the same pts twice if we are + # already fetching it. However this does not happen in practice, + # which makes sense, because different updates have different pts. + if self._state_cache.update(update, check_only=True): + # If the update doesn't have pts, fetching won't do anything. + # For example, UpdateUserStatus or UpdateChatUserTyping. try: - await self.get_me(input_peer=True) + await self._get_difference(update, channel_id, pts_date) except OSError: - pass # might not have connection - - built = EventBuilderDict(self, update, others) - for conv_set in self._conversations.values(): - for conv in conv_set: - ev = built[events.NewMessage] - if ev: - conv._on_new_message(ev) - - ev = built[events.MessageEdited] - if ev: - conv._on_edit(ev) - - ev = built[events.MessageRead] - if ev: - conv._on_read(ev) - - if conv._custom: - await conv._check_custom(built) - - for builder, callback in self._event_builders: - event = built[type(builder)] - if not event: - continue - - if not builder.resolved: - await builder.resolve(self) - - filter = builder.filter(event) - if inspect.isawaitable(filter): - filter = await filter - if not filter: - continue - - try: - await callback(event) - except errors.AlreadyInConversationError: - name = getattr(callback, '__name__', repr(callback)) - self._log[__name__].debug( - 'Event handler "%s" already has an open conversation, ' - 'ignoring new one', name) - except events.StopPropagation: - name = getattr(callback, '__name__', repr(callback)) - self._log[__name__].debug( - 'Event handler "%s" stopped chain of propagation ' - 'for event %s.', name, type(event).__name__ - ) - break - except Exception as e: - if not isinstance(e, asyncio.CancelledError) or self.is_connected(): - name = getattr(callback, '__name__', repr(callback)) - self._log[__name__].exception('Unhandled exception on %s', name) - - async def _dispatch_event(self: 'TelegramClient', event): - """ - Dispatches a single, out-of-order event. Used by `AlbumHack`. - """ - # We're duplicating a most logic from `_dispatch_update`, but all in - # the name of speed; we don't want to make it worse for all updates - # just because albums may need it. - for builder, callback in self._event_builders: - if isinstance(builder, events.Raw): - continue - if not isinstance(event, builder.Event): - continue - - if not builder.resolved: - await builder.resolve(self) - - filter = builder.filter(event) - if inspect.isawaitable(filter): - filter = await filter - if not filter: - continue - - try: - await callback(event) - except errors.AlreadyInConversationError: - name = getattr(callback, '__name__', repr(callback)) - self._log[__name__].debug( - 'Event handler "%s" already has an open conversation, ' - 'ignoring new one', name) - except events.StopPropagation: - name = getattr(callback, '__name__', repr(callback)) - self._log[__name__].debug( - 'Event handler "%s" stopped chain of propagation ' - 'for event %s.', name, type(event).__name__ - ) - break - except Exception as e: - if not isinstance(e, asyncio.CancelledError) or self.is_connected(): - name = getattr(callback, '__name__', repr(callback)) - self._log[__name__].exception('Unhandled exception on %s', name) - - async def _get_difference(self: 'TelegramClient', update, channel_id, pts_date): - """ - Get the difference for this `channel_id` if any, then load entities. - - Calls :tl:`updates.getDifference`, which fills the entities cache - (always done by `__call__`) and lets us know about the full entities. - """ - # Fetch since the last known pts/date before this update arrived, - # in order to fetch this update at full, including its entities. - self._log[__name__].debug('Getting difference for entities ' - 'for %r', update.__class__) - if channel_id: - # There are reports where we somehow call get channel difference - # with `InputPeerEmpty`. Check our assumptions to better debug - # this when it happens. - assert isinstance(channel_id, int), 'channel_id was {}, not int in {}'.format(type(channel_id), update) - try: - # Wrap the ID inside a peer to ensure we get a channel back. - where = await self.get_input_entity(types.PeerChannel(channel_id)) + pass # We were disconnected, that's okay + except errors.RPCError: + # There's a high chance the request fails because we lack + # the channel. Because these "happen sporadically" (#1428) + # we should be okay (no flood waits) even if more occur. + pass except ValueError: - # There's a high chance that this fails, since - # we are getting the difference to fetch entities. - return + # There is a chance that GetFullChannelRequest and GetDifferenceRequest + # inside the _get_difference() function will end up with + # ValueError("Request was unsuccessful N time(s)") for whatever reasons. + pass - if not pts_date: - # First-time, can't get difference. Get pts instead. - result = await self(functions.channels.GetFullChannelRequest( - utils.get_input_channel(where) - )) - self._state_cache[channel_id] = result.full_chat.pts - return - - result = await self(functions.updates.GetChannelDifferenceRequest( - channel=where, - filter=types.ChannelMessagesFilterEmpty(), - pts=pts_date, # just pts - limit=100, - force=True - )) - else: - if not pts_date[0]: - # First-time, can't get difference. Get pts instead. - result = await self(functions.updates.GetStateRequest()) - self._state_cache[None] = result.pts, result.date - return - - result = await self(functions.updates.GetDifferenceRequest( - pts=pts_date[0], - date=pts_date[1], - qts=0 - )) - - if isinstance(result, (types.updates.Difference, - types.updates.DifferenceSlice, - types.updates.ChannelDifference, - types.updates.ChannelDifferenceTooLong)): - update._entities.update({ - utils.get_peer_id(x): x for x in - itertools.chain(result.users, result.chats) - }) - - async def _handle_auto_reconnect(self: 'TelegramClient'): - # TODO Catch-up - # For now we make a high-level request to let Telegram - # know we are still interested in receiving more updates. + if not self._self_input_peer: + # Some updates require our own ID, so we must make sure + # that the event builder has offline access to it. Calling + # `get_me()` will cache it under `self._self_input_peer`. + # + # It will return `None` if we haven't logged in yet which is + # fine, we will just retry next time anyway. try: - await self.get_me() + await self.get_me(input_peer=True) + except OSError: + pass # might not have connection + + built = EventBuilderDict(self, update, others) + for conv_set in self._conversations.values(): + for conv in conv_set: + ev = built[events.NewMessage] + if ev: + conv._on_new_message(ev) + + ev = built[events.MessageEdited] + if ev: + conv._on_edit(ev) + + ev = built[events.MessageRead] + if ev: + conv._on_read(ev) + + if conv._custom: + await conv._check_custom(built) + + for builder, callback in self._event_builders: + event = built[type(builder)] + if not event: + continue + + if not builder.resolved: + await builder.resolve(self) + + filter = builder.filter(event) + if inspect.isawaitable(filter): + filter = await filter + if not filter: + continue + + try: + await callback(event) + except errors.AlreadyInConversationError: + name = getattr(callback, '__name__', repr(callback)) + self._log[__name__].debug( + 'Event handler "%s" already has an open conversation, ' + 'ignoring new one', name) + except events.StopPropagation: + name = getattr(callback, '__name__', repr(callback)) + self._log[__name__].debug( + 'Event handler "%s" stopped chain of propagation ' + 'for event %s.', name, type(event).__name__ + ) + break except Exception as e: - self._log[__name__].warning('Error executing high-level request ' - 'after reconnect: %s: %s', type(e), e) + if not isinstance(e, asyncio.CancelledError) or self.is_connected(): + name = getattr(callback, '__name__', repr(callback)) + self._log[__name__].exception('Unhandled exception on %s', name) + +async def _dispatch_event(self: 'TelegramClient', event): + """ + Dispatches a single, out-of-order event. Used by `AlbumHack`. + """ + # We're duplicating a most logic from `_dispatch_update`, but all in + # the name of speed; we don't want to make it worse for all updates + # just because albums may need it. + for builder, callback in self._event_builders: + if isinstance(builder, events.Raw): + continue + if not isinstance(event, builder.Event): + continue + + if not builder.resolved: + await builder.resolve(self) + + filter = builder.filter(event) + if inspect.isawaitable(filter): + filter = await filter + if not filter: + continue - return try: - self._log[__name__].info( - 'Asking for the current state after reconnect...') + await callback(event) + except errors.AlreadyInConversationError: + name = getattr(callback, '__name__', repr(callback)) + self._log[__name__].debug( + 'Event handler "%s" already has an open conversation, ' + 'ignoring new one', name) + except events.StopPropagation: + name = getattr(callback, '__name__', repr(callback)) + self._log[__name__].debug( + 'Event handler "%s" stopped chain of propagation ' + 'for event %s.', name, type(event).__name__ + ) + break + except Exception as e: + if not isinstance(e, asyncio.CancelledError) or self.is_connected(): + name = getattr(callback, '__name__', repr(callback)) + self._log[__name__].exception('Unhandled exception on %s', name) - # TODO consider: - # If there aren't many updates while the client is disconnected - # (I tried with up to 20), Telegram seems to send them without - # asking for them (via updates.getDifference). - # - # On disconnection, the library should probably set a "need - # difference" or "catching up" flag so that any new updates are - # ignored, and then the library should call updates.getDifference - # itself to fetch them. - # - # In any case (either there are too many updates and Telegram - # didn't send them, or there isn't a lot and Telegram sent them - # but we dropped them), we fetch the new difference to get all - # missed updates. I feel like this would be the best solution. +async def _get_difference(self: 'TelegramClient', update, channel_id, pts_date): + """ + Get the difference for this `channel_id` if any, then load entities. - # If a disconnection occurs, the old known state will be - # the latest one we were aware of, so we can catch up since - # the most recent state we were aware of. - await self.catch_up() + Calls :tl:`updates.getDifference`, which fills the entities cache + (always done by `__call__`) and lets us know about the full entities. + """ + # Fetch since the last known pts/date before this update arrived, + # in order to fetch this update at full, including its entities. + self._log[__name__].debug('Getting difference for entities ' + 'for %r', update.__class__) + if channel_id: + # There are reports where we somehow call get channel difference + # with `InputPeerEmpty`. Check our assumptions to better debug + # this when it happens. + assert isinstance(channel_id, int), 'channel_id was {}, not int in {}'.format(type(channel_id), update) + try: + # Wrap the ID inside a peer to ensure we get a channel back. + where = await self.get_input_entity(types.PeerChannel(channel_id)) + except ValueError: + # There's a high chance that this fails, since + # we are getting the difference to fetch entities. + return - self._log[__name__].info('Successfully fetched missed updates') - except errors.RPCError as e: - self._log[__name__].warning('Failed to get missed updates after ' - 'reconnect: %r', e) - except Exception: - self._log[__name__].exception( - 'Unhandled exception while getting update difference after reconnect') + if not pts_date: + # First-time, can't get difference. Get pts instead. + result = await self(functions.channels.GetFullChannelRequest( + utils.get_input_channel(where) + )) + self._state_cache[channel_id] = result.full_chat.pts + return - # endregion + result = await self(functions.updates.GetChannelDifferenceRequest( + channel=where, + filter=types.ChannelMessagesFilterEmpty(), + pts=pts_date, # just pts + limit=100, + force=True + )) + else: + if not pts_date[0]: + # First-time, can't get difference. Get pts instead. + result = await self(functions.updates.GetStateRequest()) + self._state_cache[None] = result.pts, result.date + return + + result = await self(functions.updates.GetDifferenceRequest( + pts=pts_date[0], + date=pts_date[1], + qts=0 + )) + + if isinstance(result, (types.updates.Difference, + types.updates.DifferenceSlice, + types.updates.ChannelDifference, + types.updates.ChannelDifferenceTooLong)): + update._entities.update({ + utils.get_peer_id(x): x for x in + itertools.chain(result.users, result.chats) + }) + +async def _handle_auto_reconnect(self: 'TelegramClient'): + # TODO Catch-up + # For now we make a high-level request to let Telegram + # know we are still interested in receiving more updates. + try: + await self.get_me() + except Exception as e: + self._log[__name__].warning('Error executing high-level request ' + 'after reconnect: %s: %s', type(e), e) + + return + try: + self._log[__name__].info( + 'Asking for the current state after reconnect...') + + # TODO consider: + # If there aren't many updates while the client is disconnected + # (I tried with up to 20), Telegram seems to send them without + # asking for them (via updates.getDifference). + # + # On disconnection, the library should probably set a "need + # difference" or "catching up" flag so that any new updates are + # ignored, and then the library should call updates.getDifference + # itself to fetch them. + # + # In any case (either there are too many updates and Telegram + # didn't send them, or there isn't a lot and Telegram sent them + # but we dropped them), we fetch the new difference to get all + # missed updates. I feel like this would be the best solution. + + # If a disconnection occurs, the old known state will be + # the latest one we were aware of, so we can catch up since + # the most recent state we were aware of. + await self.catch_up() + + self._log[__name__].info('Successfully fetched missed updates') + except errors.RPCError as e: + self._log[__name__].warning('Failed to get missed updates after ' + 'reconnect: %r', e) + except Exception: + self._log[__name__].exception( + 'Unhandled exception while getting update difference after reconnect') class EventBuilderDict: diff --git a/telethon/client/uploads.py b/telethon/client/uploads.py index 4c9f9d32..58f69bad 100644 --- a/telethon/client/uploads.py +++ b/telethon/client/uploads.py @@ -88,679 +88,381 @@ def _resize_photo_if_needed( file.seek(before, io.SEEK_SET) -class UploadMethods: - - # region Public methods - - async def send_file( - self: 'TelegramClient', - entity: 'hints.EntityLike', - file: 'typing.Union[hints.FileLike, typing.Sequence[hints.FileLike]]', - *, - caption: typing.Union[str, typing.Sequence[str]] = None, - force_document: bool = False, - file_size: int = None, - clear_draft: bool = False, - progress_callback: 'hints.ProgressCallback' = None, - reply_to: 'hints.MessageIDLike' = None, - attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None, - thumb: 'hints.FileLike' = None, - allow_cache: bool = True, - parse_mode: str = (), - formatting_entities: typing.Optional[typing.List[types.TypeMessageEntity]] = None, - voice_note: bool = False, - video_note: bool = False, - buttons: 'hints.MarkupLike' = None, - silent: bool = None, - background: bool = None, - supports_streaming: bool = False, - schedule: 'hints.DateLike' = None, - comment_to: 'typing.Union[int, types.Message]' = None, - ttl: int = None, - **kwargs) -> 'types.Message': - """ - Sends message with the given file to the specified entity. - - .. note:: - - If the ``hachoir3`` package (``hachoir`` module) is installed, - it will be used to determine metadata from audio and video files. - - If the ``pillow`` package is installed and you are sending a photo, - it will be resized to fit within the maximum dimensions allowed - by Telegram to avoid ``errors.PhotoInvalidDimensionsError``. This - cannot be done if you are sending :tl:`InputFile`, however. - - Arguments - entity (`entity`): - Who will receive the file. - - file (`str` | `bytes` | `file` | `media`): - The file to send, which can be one of: - - * A local file path to an in-disk file. The file name - will be the path's base name. - - * A `bytes` byte array with the file's data to send - (for example, by using ``text.encode('utf-8')``). - A default file name will be used. - - * A bytes `io.IOBase` stream over the file to send - (for example, by using ``open(file, 'rb')``). - Its ``.name`` property will be used for the file name, - or a default if it doesn't have one. - - * An external URL to a file over the internet. This will - send the file as "external" media, and Telegram is the - one that will fetch the media and send it. - - * A Bot API-like ``file_id``. You can convert previously - sent media to file IDs for later reusing with - `telethon.utils.pack_bot_file_id`. - - * A handle to an existing file (for example, if you sent a - message with media before, you can use its ``message.media`` - as a file here). - - * A handle to an uploaded file (from `upload_file`). - - * A :tl:`InputMedia` instance. For example, if you want to - send a dice use :tl:`InputMediaDice`, or if you want to - send a contact use :tl:`InputMediaContact`. - - To send an album, you should provide a list in this parameter. - - If a list or similar is provided, the files in it will be - sent as an album in the order in which they appear, sliced - in chunks of 10 if more than 10 are given. - - caption (`str`, optional): - Optional caption for the sent media message. When sending an - album, the caption may be a list of strings, which will be - assigned to the files pairwise. - - force_document (`bool`, optional): - If left to `False` and the file is a path that ends with - the extension of an image file or a video file, it will be - sent as such. Otherwise always as a document. - - file_size (`int`, optional): - The size of the file to be uploaded if it needs to be uploaded, - which will be determined automatically if not specified. - - If the file size can't be determined beforehand, the entire - file will be read in-memory to find out how large it is. - - clear_draft (`bool`, optional): - Whether the existing draft should be cleared or not. - - progress_callback (`callable`, optional): - A callback function accepting two parameters: - ``(sent bytes, total)``. - - reply_to (`int` | `Message `): - Same as `reply_to` from `send_message`. - - attributes (`list`, optional): - Optional attributes that override the inferred ones, like - :tl:`DocumentAttributeFilename` and so on. - - thumb (`str` | `bytes` | `file`, optional): - Optional JPEG thumbnail (for documents). **Telegram will - ignore this parameter** unless you pass a ``.jpg`` file! - - The file must also be small in dimensions and in disk size. - Successful thumbnails were files below 20kB and 320x320px. - Width/height and dimensions/size ratios may be important. - For Telegram to accept a thumbnail, you must provide the - dimensions of the underlying media through ``attributes=`` - with :tl:`DocumentAttributesVideo` or by installing the - optional ``hachoir`` dependency. - - - allow_cache (`bool`, optional): - This parameter currently does nothing, but is kept for - backward-compatibility (and it may get its use back in - the future). - - parse_mode (`object`, optional): - See the `TelegramClient.parse_mode - ` - property for allowed values. Markdown parsing will be used by - default. - - formatting_entities (`list`, optional): - A list of message formatting entities. When provided, the ``parse_mode`` is ignored. - - voice_note (`bool`, optional): - If `True` the audio will be sent as a voice note. - - video_note (`bool`, optional): - If `True` the video will be sent as a video note, - also known as a round video message. - - buttons (`list`, `custom.Button `, :tl:`KeyboardButton`): - The matrix (list of lists), row list or button to be shown - after sending the message. This parameter will only work if - you have signed in as a bot. You can also pass your own - :tl:`ReplyMarkup` here. - - silent (`bool`, optional): - Whether the message should notify people with sound or not. - Defaults to `False` (send with a notification sound unless - the person has the chat muted). Set it to `True` to alter - this behaviour. - - background (`bool`, optional): - Whether the message should be send in background. - - supports_streaming (`bool`, optional): - Whether the sent video supports streaming or not. Note that - Telegram only recognizes as streamable some formats like MP4, - and others like AVI or MKV will not work. You should convert - these to MP4 before sending if you want them to be streamable. - Unsupported formats will result in ``VideoContentTypeError``. - - schedule (`hints.DateLike`, optional): - If set, the file won't send immediately, and instead - it will be scheduled to be automatically sent at a later - time. - - comment_to (`int` | `Message `, optional): - Similar to ``reply_to``, but replies in the linked group of a - broadcast channel instead (effectively leaving a "comment to" - the specified message). - - This parameter takes precedence over ``reply_to``. If there is - no linked chat, `telethon.errors.sgIdInvalidError` is raised. - - ttl (`int`. optional): - The Time-To-Live of the file (also known as "self-destruct timer" - or "self-destructing media"). If set, files can only be viewed for - a short period of time before they disappear from the message - history automatically. - - The value must be at least 1 second, and at most 60 seconds, - otherwise Telegram will ignore this parameter. - - Not all types of media can be used with this parameter, such - as text documents, which will fail with ``TtlMediaInvalidError``. - - Returns - The `Message ` (or messages) - containing the sent file, or messages if a list of them was passed. - - Example - .. code-block:: python - - # Normal files like photos - await client.send_file(chat, '/my/photos/me.jpg', caption="It's me!") - # or - await client.send_message(chat, "It's me!", file='/my/photos/me.jpg') - - # Voice notes or round videos - await client.send_file(chat, '/my/songs/song.mp3', voice_note=True) - await client.send_file(chat, '/my/videos/video.mp4', video_note=True) - - # Custom thumbnails - await client.send_file(chat, '/my/documents/doc.txt', thumb='photo.jpg') - - # Only documents - await client.send_file(chat, '/my/photos/photo.png', force_document=True) - - # Albums - await client.send_file(chat, [ - '/my/photos/holiday1.jpg', - '/my/photos/holiday2.jpg', - '/my/drawings/portrait.png' - ]) - - # Printing upload progress - def callback(current, total): - print('Uploaded', current, 'out of', total, - 'bytes: {:.2%}'.format(current / total)) - - await client.send_file(chat, file, progress_callback=callback) - - # Dices, including dart and other future emoji - from telethon.tl import types - await client.send_file(chat, types.InputMediaDice('')) - await client.send_file(chat, types.InputMediaDice('🎯')) - - # Contacts - await client.send_file(chat, types.InputMediaContact( - phone_number='+34 123 456 789', - first_name='Example', - last_name='', - vcard='' - )) - """ - # TODO Properly implement allow_cache to reuse the sha256 of the file - # i.e. `None` was used - if not file: - raise TypeError('Cannot use {!r} as file'.format(file)) - - if not caption: - caption = '' - - entity = await self.get_input_entity(entity) - if comment_to is not None: - entity, reply_to = await self._get_comment_data(entity, comment_to) - else: - reply_to = utils.get_message_id(reply_to) - - # First check if the user passed an iterable, in which case - # we may want to send grouped. - if utils.is_list_like(file): - if utils.is_list_like(caption): - captions = caption - else: - captions = [caption] - - result = [] - while file: - result += await self._send_album( - entity, file[:10], caption=captions[:10], - progress_callback=progress_callback, reply_to=reply_to, - parse_mode=parse_mode, silent=silent, schedule=schedule, - supports_streaming=supports_streaming, clear_draft=clear_draft, - force_document=force_document, background=background, - ) - file = file[10:] - captions = captions[10:] - - for doc, cap in zip(file, captions): - result.append(await self.send_file( - entity, doc, allow_cache=allow_cache, - caption=cap, force_document=force_document, - progress_callback=progress_callback, reply_to=reply_to, - attributes=attributes, thumb=thumb, voice_note=voice_note, - video_note=video_note, buttons=buttons, silent=silent, - supports_streaming=supports_streaming, schedule=schedule, - clear_draft=clear_draft, background=background, - **kwargs - )) - - return result - - if formatting_entities is not None: - msg_entities = formatting_entities - else: - caption, msg_entities =\ - await self._parse_message_text(caption, parse_mode) - - file_handle, media, image = await self._file_to_media( - file, force_document=force_document, - file_size=file_size, - progress_callback=progress_callback, - attributes=attributes, allow_cache=allow_cache, thumb=thumb, - voice_note=voice_note, video_note=video_note, - supports_streaming=supports_streaming, ttl=ttl - ) - - # e.g. invalid cast from :tl:`MessageMediaWebPage` - if not media: - raise TypeError('Cannot use {!r} as file'.format(file)) - - markup = self.build_reply_markup(buttons) - request = functions.messages.SendMediaRequest( - entity, media, reply_to_msg_id=reply_to, message=caption, - entities=msg_entities, reply_markup=markup, silent=silent, - schedule_date=schedule, clear_draft=clear_draft, - background=background - ) - return self._get_response_message(request, await self(request), entity) - - async def _send_album(self: 'TelegramClient', entity, files, caption='', - progress_callback=None, reply_to=None, - parse_mode=(), silent=None, schedule=None, - supports_streaming=None, clear_draft=None, - force_document=False, background=None, ttl=None): - """Specialized version of .send_file for albums""" - # We don't care if the user wants to avoid cache, we will use it - # anyway. Why? The cached version will be exactly the same thing - # we need to produce right now to send albums (uploadMedia), and - # cache only makes a difference for documents where the user may - # want the attributes used on them to change. - # - # In theory documents can be sent inside the albums but they appear - # as different messages (not inside the album), and the logic to set - # the attributes/avoid cache is already written in .send_file(). - entity = await self.get_input_entity(entity) - if not utils.is_list_like(caption): - caption = (caption,) - - captions = [] - for c in reversed(caption): # Pop from the end (so reverse) - captions.append(await self._parse_message_text(c or '', parse_mode)) - +async def send_file( + self: 'TelegramClient', + entity: 'hints.EntityLike', + file: 'typing.Union[hints.FileLike, typing.Sequence[hints.FileLike]]', + *, + caption: typing.Union[str, typing.Sequence[str]] = None, + force_document: bool = False, + file_size: int = None, + clear_draft: bool = False, + progress_callback: 'hints.ProgressCallback' = None, + reply_to: 'hints.MessageIDLike' = None, + attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None, + thumb: 'hints.FileLike' = None, + allow_cache: bool = True, + parse_mode: str = (), + formatting_entities: typing.Optional[typing.List[types.TypeMessageEntity]] = None, + voice_note: bool = False, + video_note: bool = False, + buttons: 'hints.MarkupLike' = None, + silent: bool = None, + background: bool = None, + supports_streaming: bool = False, + schedule: 'hints.DateLike' = None, + comment_to: 'typing.Union[int, types.Message]' = None, + ttl: int = None, + **kwargs) -> 'types.Message': + # TODO Properly implement allow_cache to reuse the sha256 of the file + # i.e. `None` was used + if not file: + raise TypeError('Cannot use {!r} as file'.format(file)) + + if not caption: + caption = '' + + entity = await self.get_input_entity(entity) + if comment_to is not None: + entity, reply_to = await self._get_comment_data(entity, comment_to) + else: reply_to = utils.get_message_id(reply_to) - # Need to upload the media first, but only if they're not cached yet - media = [] - for file in files: - # Albums want :tl:`InputMedia` which, in theory, includes - # :tl:`InputMediaUploadedPhoto`. However using that will - # make it `raise MediaInvalidError`, so we need to upload - # it as media and then convert that to :tl:`InputMediaPhoto`. - fh, fm, _ = await self._file_to_media( - file, supports_streaming=supports_streaming, - force_document=force_document, ttl=ttl) - if isinstance(fm, (types.InputMediaUploadedPhoto, types.InputMediaPhotoExternal)): - r = await self(functions.messages.UploadMediaRequest( - entity, media=fm - )) + # First check if the user passed an iterable, in which case + # we may want to send grouped. + if utils.is_list_like(file): + if utils.is_list_like(caption): + captions = caption + else: + captions = [caption] - fm = utils.get_input_media(r.photo) - elif isinstance(fm, types.InputMediaUploadedDocument): - r = await self(functions.messages.UploadMediaRequest( - entity, media=fm - )) + result = [] + while file: + result += await self._send_album( + entity, file[:10], caption=captions[:10], + progress_callback=progress_callback, reply_to=reply_to, + parse_mode=parse_mode, silent=silent, schedule=schedule, + supports_streaming=supports_streaming, clear_draft=clear_draft, + force_document=force_document, background=background, + ) + file = file[10:] + captions = captions[10:] - fm = utils.get_input_media( - r.document, supports_streaming=supports_streaming) - - if captions: - caption, msg_entities = captions.pop() - else: - caption, msg_entities = '', None - media.append(types.InputSingleMedia( - fm, - message=caption, - entities=msg_entities - # random_id is autogenerated + for doc, cap in zip(file, captions): + result.append(await self.send_file( + entity, doc, allow_cache=allow_cache, + caption=cap, force_document=force_document, + progress_callback=progress_callback, reply_to=reply_to, + attributes=attributes, thumb=thumb, voice_note=voice_note, + video_note=video_note, buttons=buttons, silent=silent, + supports_streaming=supports_streaming, schedule=schedule, + clear_draft=clear_draft, background=background, + **kwargs )) - # Now we can construct the multi-media request - request = functions.messages.SendMultiMediaRequest( - entity, reply_to_msg_id=reply_to, multi_media=media, - silent=silent, schedule_date=schedule, clear_draft=clear_draft, - background=background - ) - result = await self(request) + return result - random_ids = [m.random_id for m in media] - return self._get_response_message(random_ids, result, entity) + if formatting_entities is not None: + msg_entities = formatting_entities + else: + caption, msg_entities =\ + await self._parse_message_text(caption, parse_mode) - async def upload_file( - self: 'TelegramClient', - file: 'hints.FileLike', - *, - part_size_kb: float = None, - file_size: int = None, - file_name: str = None, - use_cache: type = None, - key: bytes = None, - iv: bytes = None, - progress_callback: 'hints.ProgressCallback' = None) -> 'types.TypeInputFile': - """ - Uploads a file to Telegram's servers, without sending it. + file_handle, media, image = await self._file_to_media( + file, force_document=force_document, + file_size=file_size, + progress_callback=progress_callback, + attributes=attributes, allow_cache=allow_cache, thumb=thumb, + voice_note=voice_note, video_note=video_note, + supports_streaming=supports_streaming, ttl=ttl + ) - .. note:: + # e.g. invalid cast from :tl:`MessageMediaWebPage` + if not media: + raise TypeError('Cannot use {!r} as file'.format(file)) - Generally, you want to use `send_file` instead. + markup = self.build_reply_markup(buttons) + request = functions.messages.SendMediaRequest( + entity, media, reply_to_msg_id=reply_to, message=caption, + entities=msg_entities, reply_markup=markup, silent=silent, + schedule_date=schedule, clear_draft=clear_draft, + background=background + ) + return self._get_response_message(request, await self(request), entity) - This method returns a handle (an instance of :tl:`InputFile` or - :tl:`InputFileBig`, as required) which can be later used before - it expires (they are usable during less than a day). +async def _send_album(self: 'TelegramClient', entity, files, caption='', + progress_callback=None, reply_to=None, + parse_mode=(), silent=None, schedule=None, + supports_streaming=None, clear_draft=None, + force_document=False, background=None, ttl=None): + """Specialized version of .send_file for albums""" + # We don't care if the user wants to avoid cache, we will use it + # anyway. Why? The cached version will be exactly the same thing + # we need to produce right now to send albums (uploadMedia), and + # cache only makes a difference for documents where the user may + # want the attributes used on them to change. + # + # In theory documents can be sent inside the albums but they appear + # as different messages (not inside the album), and the logic to set + # the attributes/avoid cache is already written in .send_file(). + entity = await self.get_input_entity(entity) + if not utils.is_list_like(caption): + caption = (caption,) - Uploading a file will simply return a "handle" to the file stored - remotely in the Telegram servers, which can be later used on. This - will **not** upload the file to your own chat or any chat at all. + captions = [] + for c in reversed(caption): # Pop from the end (so reverse) + captions.append(await self._parse_message_text(c or '', parse_mode)) - Arguments - file (`str` | `bytes` | `file`): - The path of the file, byte array, or stream that will be sent. - Note that if a byte array or a stream is given, a filename - or its type won't be inferred, and it will be sent as an - "unnamed application/octet-stream". + reply_to = utils.get_message_id(reply_to) - part_size_kb (`int`, optional): - Chunk size when uploading files. The larger, the less - requests will be made (up to 512KB maximum). + # Need to upload the media first, but only if they're not cached yet + media = [] + for file in files: + # Albums want :tl:`InputMedia` which, in theory, includes + # :tl:`InputMediaUploadedPhoto`. However using that will + # make it `raise MediaInvalidError`, so we need to upload + # it as media and then convert that to :tl:`InputMediaPhoto`. + fh, fm, _ = await self._file_to_media( + file, supports_streaming=supports_streaming, + force_document=force_document, ttl=ttl) + if isinstance(fm, (types.InputMediaUploadedPhoto, types.InputMediaPhotoExternal)): + r = await self(functions.messages.UploadMediaRequest( + entity, media=fm + )) - file_size (`int`, optional): - The size of the file to be uploaded, which will be determined - automatically if not specified. + fm = utils.get_input_media(r.photo) + elif isinstance(fm, types.InputMediaUploadedDocument): + r = await self(functions.messages.UploadMediaRequest( + entity, media=fm + )) - If the file size can't be determined beforehand, the entire - file will be read in-memory to find out how large it is. + fm = utils.get_input_media( + r.document, supports_streaming=supports_streaming) - file_name (`str`, optional): - The file name which will be used on the resulting InputFile. - If not specified, the name will be taken from the ``file`` - and if this is not a `str`, it will be ``"unnamed"``. + if captions: + caption, msg_entities = captions.pop() + else: + caption, msg_entities = '', None + media.append(types.InputSingleMedia( + fm, + message=caption, + entities=msg_entities + # random_id is autogenerated + )) - use_cache (`type`, optional): - This parameter currently does nothing, but is kept for - backward-compatibility (and it may get its use back in - the future). + # Now we can construct the multi-media request + request = functions.messages.SendMultiMediaRequest( + entity, reply_to_msg_id=reply_to, multi_media=media, + silent=silent, schedule_date=schedule, clear_draft=clear_draft, + background=background + ) + result = await self(request) - key ('bytes', optional): - In case of an encrypted upload (secret chats) a key is supplied + random_ids = [m.random_id for m in media] + return self._get_response_message(random_ids, result, entity) - iv ('bytes', optional): - In case of an encrypted upload (secret chats) an iv is supplied +async def upload_file( + self: 'TelegramClient', + file: 'hints.FileLike', + *, + part_size_kb: float = None, + file_size: int = None, + file_name: str = None, + use_cache: type = None, + key: bytes = None, + iv: bytes = None, + progress_callback: 'hints.ProgressCallback' = None) -> 'types.TypeInputFile': + if isinstance(file, (types.InputFile, types.InputFileBig)): + return file # Already uploaded - progress_callback (`callable`, optional): - A callback function accepting two parameters: - ``(sent bytes, total)``. + pos = 0 + async with helpers._FileStream(file, file_size=file_size) as stream: + # Opening the stream will determine the correct file size + file_size = stream.file_size - Returns - :tl:`InputFileBig` if the file size is larger than 10MB, - `InputSizedFile ` - (subclass of :tl:`InputFile`) otherwise. + if not part_size_kb: + part_size_kb = utils.get_appropriated_part_size(file_size) - Example - .. code-block:: python + if part_size_kb > 512: + raise ValueError('The part size must be less or equal to 512KB') - # Photos as photo and document - file = await client.upload_file('photo.jpg') - await client.send_file(chat, file) # sends as photo - await client.send_file(chat, file, force_document=True) # sends as document + part_size = int(part_size_kb * 1024) + if part_size % 1024 != 0: + raise ValueError( + 'The part size must be evenly divisible by 1024') - file.name = 'not a photo.jpg' - await client.send_file(chat, file, force_document=True) # document, new name + # Set a default file name if None was specified + file_id = helpers.generate_random_long() + if not file_name: + file_name = stream.name or str(file_id) - # As song or as voice note - file = await client.upload_file('song.ogg') - await client.send_file(chat, file) # sends as song - await client.send_file(chat, file, voice_note=True) # sends as voice note - """ - if isinstance(file, (types.InputFile, types.InputFileBig)): - return file # Already uploaded + # If the file name lacks extension, add it if possible. + # Else Telegram complains with `PHOTO_EXT_INVALID_ERROR` + # even if the uploaded image is indeed a photo. + if not os.path.splitext(file_name)[-1]: + file_name += utils._get_extension(stream) + + # Determine whether the file is too big (over 10MB) or not + # Telegram does make a distinction between smaller or larger files + is_big = file_size > 10 * 1024 * 1024 + hash_md5 = hashlib.md5() + + part_count = (file_size + part_size - 1) // part_size + self._log[__name__].info('Uploading file of %d bytes in %d chunks of %d', + file_size, part_count, part_size) pos = 0 - async with helpers._FileStream(file, file_size=file_size) as stream: - # Opening the stream will determine the correct file size - file_size = stream.file_size + for part_index in range(part_count): + # Read the file by in chunks of size part_size + part = await helpers._maybe_await(stream.read(part_size)) - if not part_size_kb: - part_size_kb = utils.get_appropriated_part_size(file_size) + if not isinstance(part, bytes): + raise TypeError( + 'file descriptor returned {}, not bytes (you must ' + 'open the file in bytes mode)'.format(type(part))) - if part_size_kb > 512: - raise ValueError('The part size must be less or equal to 512KB') - - part_size = int(part_size_kb * 1024) - if part_size % 1024 != 0: + # `file_size` could be wrong in which case `part` may not be + # `part_size` before reaching the end. + if len(part) != part_size and part_index < part_count - 1: raise ValueError( - 'The part size must be evenly divisible by 1024') + 'read less than {} before reaching the end; either ' + '`file_size` or `read` are wrong'.format(part_size)) - # Set a default file name if None was specified - file_id = helpers.generate_random_long() - if not file_name: - file_name = stream.name or str(file_id) + pos += len(part) - # If the file name lacks extension, add it if possible. - # Else Telegram complains with `PHOTO_EXT_INVALID_ERROR` - # even if the uploaded image is indeed a photo. - if not os.path.splitext(file_name)[-1]: - file_name += utils._get_extension(stream) + # Encryption part if needed + if key and iv: + part = AES.encrypt_ige(part, key, iv) - # Determine whether the file is too big (over 10MB) or not - # Telegram does make a distinction between smaller or larger files - is_big = file_size > 10 * 1024 * 1024 - hash_md5 = hashlib.md5() + if not is_big: + # Bit odd that MD5 is only needed for small files and not + # big ones with more chance for corruption, but that's + # what Telegram wants. + hash_md5.update(part) - part_count = (file_size + part_size - 1) // part_size - self._log[__name__].info('Uploading file of %d bytes in %d chunks of %d', - file_size, part_count, part_size) - - pos = 0 - for part_index in range(part_count): - # Read the file by in chunks of size part_size - part = await helpers._maybe_await(stream.read(part_size)) - - if not isinstance(part, bytes): - raise TypeError( - 'file descriptor returned {}, not bytes (you must ' - 'open the file in bytes mode)'.format(type(part))) - - # `file_size` could be wrong in which case `part` may not be - # `part_size` before reaching the end. - if len(part) != part_size and part_index < part_count - 1: - raise ValueError( - 'read less than {} before reaching the end; either ' - '`file_size` or `read` are wrong'.format(part_size)) - - pos += len(part) - - # Encryption part if needed - if key and iv: - part = AES.encrypt_ige(part, key, iv) - - if not is_big: - # Bit odd that MD5 is only needed for small files and not - # big ones with more chance for corruption, but that's - # what Telegram wants. - hash_md5.update(part) - - # The SavePartRequest is different depending on whether - # the file is too large or not (over or less than 10MB) - if is_big: - request = functions.upload.SaveBigFilePartRequest( - file_id, part_index, part_count, part) - else: - request = functions.upload.SaveFilePartRequest( - file_id, part_index, part) - - result = await self(request) - if result: - self._log[__name__].debug('Uploaded %d/%d', - part_index + 1, part_count) - if progress_callback: - await helpers._maybe_await(progress_callback(pos, file_size)) - else: - raise RuntimeError( - 'Failed to upload file part {}.'.format(part_index)) - - if is_big: - return types.InputFileBig(file_id, part_count, file_name) - else: - return custom.InputSizedFile( - file_id, part_count, file_name, md5=hash_md5, size=file_size - ) - - # endregion - - async def _file_to_media( - self, file, force_document=False, file_size=None, - progress_callback=None, attributes=None, thumb=None, - allow_cache=True, voice_note=False, video_note=False, - supports_streaming=False, mime_type=None, as_image=None, - ttl=None): - if not file: - return None, None, None - - if isinstance(file, pathlib.Path): - file = str(file.absolute()) - - is_image = utils.is_image(file) - if as_image is None: - as_image = is_image and not force_document - - # `aiofiles` do not base `io.IOBase` but do have `read`, so we - # just check for the read attribute to see if it's file-like. - if not isinstance(file, (str, bytes, types.InputFile, types.InputFileBig))\ - and not hasattr(file, 'read'): - # The user may pass a Message containing media (or the media, - # or anything similar) that should be treated as a file. Try - # getting the input media for whatever they passed and send it. - # - # We pass all attributes since these will be used if the user - # passed :tl:`InputFile`, and all information may be relevant. - try: - return (None, utils.get_input_media( - file, - is_photo=as_image, - attributes=attributes, - force_document=force_document, - voice_note=voice_note, - video_note=video_note, - supports_streaming=supports_streaming, - ttl=ttl - ), as_image) - except TypeError: - # Can't turn whatever was given into media - return None, None, as_image - - media = None - file_handle = None - - if isinstance(file, (types.InputFile, types.InputFileBig)): - file_handle = file - elif not isinstance(file, str) or os.path.isfile(file): - file_handle = await self.upload_file( - _resize_photo_if_needed(file, as_image), - file_size=file_size, - progress_callback=progress_callback - ) - elif re.match('https?://', file): - if as_image: - media = types.InputMediaPhotoExternal(file, ttl_seconds=ttl) + # The SavePartRequest is different depending on whether + # the file is too large or not (over or less than 10MB) + if is_big: + request = functions.upload.SaveBigFilePartRequest( + file_id, part_index, part_count, part) else: - media = types.InputMediaDocumentExternal(file, ttl_seconds=ttl) - else: - bot_file = utils.resolve_bot_file_id(file) - if bot_file: - media = utils.get_input_media(bot_file, ttl=ttl) + request = functions.upload.SaveFilePartRequest( + file_id, part_index, part) - if media: - pass # Already have media, don't check the rest - elif not file_handle: - raise ValueError( - 'Failed to convert {} to media. Not an existing file, ' - 'an HTTP URL or a valid bot-API-like file ID'.format(file) - ) - elif as_image: - media = types.InputMediaUploadedPhoto(file_handle, ttl_seconds=ttl) - else: - attributes, mime_type = utils.get_attributes( + result = await self(request) + if result: + self._log[__name__].debug('Uploaded %d/%d', + part_index + 1, part_count) + if progress_callback: + await helpers._maybe_await(progress_callback(pos, file_size)) + else: + raise RuntimeError( + 'Failed to upload file part {}.'.format(part_index)) + + if is_big: + return types.InputFileBig(file_id, part_count, file_name) + else: + return custom.InputSizedFile( + file_id, part_count, file_name, md5=hash_md5, size=file_size + ) + + +async def _file_to_media( + self, file, force_document=False, file_size=None, + progress_callback=None, attributes=None, thumb=None, + allow_cache=True, voice_note=False, video_note=False, + supports_streaming=False, mime_type=None, as_image=None, + ttl=None): + if not file: + return None, None, None + + if isinstance(file, pathlib.Path): + file = str(file.absolute()) + + is_image = utils.is_image(file) + if as_image is None: + as_image = is_image and not force_document + + # `aiofiles` do not base `io.IOBase` but do have `read`, so we + # just check for the read attribute to see if it's file-like. + if not isinstance(file, (str, bytes, types.InputFile, types.InputFileBig))\ + and not hasattr(file, 'read'): + # The user may pass a Message containing media (or the media, + # or anything similar) that should be treated as a file. Try + # getting the input media for whatever they passed and send it. + # + # We pass all attributes since these will be used if the user + # passed :tl:`InputFile`, and all information may be relevant. + try: + return (None, utils.get_input_media( file, - mime_type=mime_type, + is_photo=as_image, attributes=attributes, - force_document=force_document and not is_image, + force_document=force_document, voice_note=voice_note, video_note=video_note, supports_streaming=supports_streaming, - thumb=thumb - ) + ttl=ttl + ), as_image) + except TypeError: + # Can't turn whatever was given into media + return None, None, as_image - if not thumb: - thumb = None - else: - if isinstance(thumb, pathlib.Path): - thumb = str(thumb.absolute()) - thumb = await self.upload_file(thumb, file_size=file_size) + media = None + file_handle = None - media = types.InputMediaUploadedDocument( - file=file_handle, - mime_type=mime_type, - attributes=attributes, - thumb=thumb, - force_file=force_document and not is_image, - ttl_seconds=ttl - ) - return file_handle, media, as_image + if isinstance(file, (types.InputFile, types.InputFileBig)): + file_handle = file + elif not isinstance(file, str) or os.path.isfile(file): + file_handle = await self.upload_file( + _resize_photo_if_needed(file, as_image), + file_size=file_size, + progress_callback=progress_callback + ) + elif re.match('https?://', file): + if as_image: + media = types.InputMediaPhotoExternal(file, ttl_seconds=ttl) + else: + media = types.InputMediaDocumentExternal(file, ttl_seconds=ttl) + else: + bot_file = utils.resolve_bot_file_id(file) + if bot_file: + media = utils.get_input_media(bot_file, ttl=ttl) - # endregion + if media: + pass # Already have media, don't check the rest + elif not file_handle: + raise ValueError( + 'Failed to convert {} to media. Not an existing file, ' + 'an HTTP URL or a valid bot-API-like file ID'.format(file) + ) + elif as_image: + media = types.InputMediaUploadedPhoto(file_handle, ttl_seconds=ttl) + else: + attributes, mime_type = utils.get_attributes( + file, + mime_type=mime_type, + attributes=attributes, + force_document=force_document and not is_image, + voice_note=voice_note, + video_note=video_note, + supports_streaming=supports_streaming, + thumb=thumb + ) + + if not thumb: + thumb = None + else: + if isinstance(thumb, pathlib.Path): + thumb = str(thumb.absolute()) + thumb = await self.upload_file(thumb, file_size=file_size) + + media = types.InputMediaUploadedDocument( + file=file_handle, + mime_type=mime_type, + attributes=attributes, + thumb=thumb, + force_file=force_document and not is_image, + ttl_seconds=ttl + ) + return file_handle, media, as_image diff --git a/telethon/client/users.py b/telethon/client/users.py index 22db969e..e6964e55 100644 --- a/telethon/client/users.py +++ b/telethon/client/users.py @@ -25,587 +25,576 @@ def _fmt_flood(delay, request, *, early=False, td=datetime.timedelta): ) -class UserMethods: - async def __call__(self: 'TelegramClient', request, ordered=False, flood_sleep_threshold=None): - return await self._call(self._sender, request, ordered=ordered) +async def call(self: 'TelegramClient', sender, request, ordered=False, flood_sleep_threshold=None): + if flood_sleep_threshold is None: + flood_sleep_threshold = self.flood_sleep_threshold + requests = (request if utils.is_list_like(request) else (request,)) + for r in requests: + if not isinstance(r, TLRequest): + raise _NOT_A_REQUEST() + await r.resolve(self, utils) - async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sleep_threshold=None): - if flood_sleep_threshold is None: - flood_sleep_threshold = self.flood_sleep_threshold - requests = (request if utils.is_list_like(request) else (request,)) - for r in requests: - if not isinstance(r, TLRequest): - raise _NOT_A_REQUEST() - await r.resolve(self, utils) + # Avoid making the request if it's already in a flood wait + if r.CONSTRUCTOR_ID in self._flood_waited_requests: + due = self._flood_waited_requests[r.CONSTRUCTOR_ID] + diff = round(due - time.time()) + if diff <= 3: # Flood waits below 3 seconds are "ignored" + self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None) + elif diff <= flood_sleep_threshold: + self._log[__name__].info(*_fmt_flood(diff, r, early=True)) + await asyncio.sleep(diff) + self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None) + else: + raise errors.FloodWaitError(request=r, capture=diff) - # Avoid making the request if it's already in a flood wait - if r.CONSTRUCTOR_ID in self._flood_waited_requests: - due = self._flood_waited_requests[r.CONSTRUCTOR_ID] - diff = round(due - time.time()) - if diff <= 3: # Flood waits below 3 seconds are "ignored" - self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None) - elif diff <= flood_sleep_threshold: - self._log[__name__].info(*_fmt_flood(diff, r, early=True)) - await asyncio.sleep(diff) - self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None) - else: - raise errors.FloodWaitError(request=r, capture=diff) + if self._no_updates: + r = functions.InvokeWithoutUpdatesRequest(r) - if self._no_updates: - r = functions.InvokeWithoutUpdatesRequest(r) + request_index = 0 + last_error = None + self._last_request = time.time() - request_index = 0 - last_error = None - self._last_request = time.time() - - for attempt in retry_range(self._request_retries): - try: - future = sender.send(request, ordered=ordered) - if isinstance(future, list): - results = [] - exceptions = [] - for f in future: - try: - result = await f - except RPCError as e: - exceptions.append(e) - results.append(None) - continue - self.session.process_entities(result) - self._entity_cache.add(result) - exceptions.append(None) - results.append(result) - request_index += 1 - if any(x is not None for x in exceptions): - raise MultiError(exceptions, results, requests) - else: - return results - else: - result = await future + for attempt in retry_range(self._request_retries): + try: + future = sender.send(request, ordered=ordered) + if isinstance(future, list): + results = [] + exceptions = [] + for f in future: + try: + result = await f + except RPCError as e: + exceptions.append(e) + results.append(None) + continue self.session.process_entities(result) self._entity_cache.add(result) - return result - except (errors.ServerError, errors.RpcCallFailError, - errors.RpcMcgetFailError, errors.InterdcCallErrorError, - errors.InterdcCallRichErrorError) as e: - last_error = e - self._log[__name__].warning( - 'Telegram is having internal issues %s: %s', - e.__class__.__name__, e) - - await asyncio.sleep(2) - except (errors.FloodWaitError, errors.SlowModeWaitError, errors.FloodTestPhoneWaitError) as e: - last_error = e - if utils.is_list_like(request): - request = request[request_index] - - # SLOW_MODE_WAIT is chat-specific, not request-specific - if not isinstance(e, errors.SlowModeWaitError): - self._flood_waited_requests\ - [request.CONSTRUCTOR_ID] = time.time() + e.seconds - - # In test servers, FLOOD_WAIT_0 has been observed, and sleeping for - # such a short amount will cause retries very fast leading to issues. - if e.seconds == 0: - e.seconds = 1 - - if e.seconds <= self.flood_sleep_threshold: - self._log[__name__].info(*_fmt_flood(e.seconds, request)) - await asyncio.sleep(e.seconds) + exceptions.append(None) + results.append(result) + request_index += 1 + if any(x is not None for x in exceptions): + raise MultiError(exceptions, results, requests) else: - raise - except (errors.PhoneMigrateError, errors.NetworkMigrateError, - errors.UserMigrateError) as e: - last_error = e - self._log[__name__].info('Phone migrated to %d', e.new_dc) - should_raise = isinstance(e, ( - errors.PhoneMigrateError, errors.NetworkMigrateError - )) - if should_raise and await self.is_user_authorized(): - raise - await self._switch_dc(e.new_dc) - - if self._raise_last_call_error and last_error is not None: - raise last_error - raise ValueError('Request was unsuccessful {} time(s)' - .format(attempt)) - - # region Public methods - - async def get_me(self: 'TelegramClient', input_peer: bool = False) \ - -> 'typing.Union[types.User, types.InputPeerUser]': - """ - Gets "me", the current :tl:`User` who is logged in. - - If the user has not logged in yet, this method returns `None`. - - Arguments - input_peer (`bool`, optional): - Whether to return the :tl:`InputPeerUser` version or the normal - :tl:`User`. This can be useful if you just need to know the ID - of yourself. - - Returns - Your own :tl:`User`. - - Example - .. code-block:: python - - me = await client.get_me() - print(me.username) - """ - if input_peer and self._self_input_peer: - return self._self_input_peer - - try: - me = (await self( - functions.users.GetUsersRequest([types.InputUserSelf()])))[0] - - self._bot = me.bot - if not self._self_input_peer: - self._self_input_peer = utils.get_input_peer( - me, allow_self=False - ) - - return self._self_input_peer if input_peer else me - except errors.UnauthorizedError: - return None - - @property - def _self_id(self: 'TelegramClient') -> typing.Optional[int]: - """ - Returns the ID of the logged-in user, if known. - - This property is used in every update, and some like `updateLoginToken` - occur prior to login, so it gracefully handles when no ID is known yet. - """ - return self._self_input_peer.user_id if self._self_input_peer else None - - async def is_bot(self: 'TelegramClient') -> bool: - """ - Return `True` if the signed-in user is a bot, `False` otherwise. - - Example - .. code-block:: python - - if await client.is_bot(): - print('Beep') - else: - print('Hello') - """ - if self._bot is None: - self._bot = (await self.get_me()).bot - - return self._bot - - async def is_user_authorized(self: 'TelegramClient') -> bool: - """ - Returns `True` if the user is authorized (logged in). - - Example - .. code-block:: python - - if not await client.is_user_authorized(): - await client.send_code_request(phone) - code = input('enter code: ') - await client.sign_in(phone, code) - """ - if self._authorized is None: - try: - # Any request that requires authorization will work - await self(functions.updates.GetStateRequest()) - self._authorized = True - except errors.RPCError: - self._authorized = False - - return self._authorized - - async def get_entity( - self: 'TelegramClient', - entity: 'hints.EntitiesLike') -> 'hints.Entity': - """ - Turns the given entity into a valid Telegram :tl:`User`, :tl:`Chat` - or :tl:`Channel`. You can also pass a list or iterable of entities, - and they will be efficiently fetched from the network. - - Arguments - entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`): - If a username is given, **the username will be resolved** making - an API call every time. Resolving usernames is an expensive - operation and will start hitting flood waits around 50 usernames - in a short period of time. - - If you want to get the entity for a *cached* username, you should - first `get_input_entity(username) ` which will - use the cache), and then use `get_entity` with the result of the - previous call. - - Similar limits apply to invite links, and you should use their - ID instead. - - Using phone numbers (from people in your contact list), exact - names, integer IDs or :tl:`Peer` rely on a `get_input_entity` - first, which in turn needs the entity to be in cache, unless - a :tl:`InputPeer` was passed. - - Unsupported types will raise ``TypeError``. - - If the entity can't be found, ``ValueError`` will be raised. - - Returns - :tl:`User`, :tl:`Chat` or :tl:`Channel` corresponding to the - input entity. A list will be returned if more than one was given. - - Example - .. code-block:: python - - from telethon import utils - - me = await client.get_entity('me') - print(utils.get_display_name(me)) - - chat = await client.get_input_entity('username') - async for message in client.iter_messages(chat): - ... - - # Note that you could have used the username directly, but it's - # good to use get_input_entity if you will reuse it a lot. - async for message in client.iter_messages('username'): - ... - - # Note that for this to work the phone number must be in your contacts - some_id = await client.get_peer_id('+34123456789') - """ - single = not utils.is_list_like(entity) - if single: - entity = (entity,) - - # Group input entities by string (resolve username), - # input users (get users), input chat (get chats) and - # input channels (get channels) to get the most entities - # in the less amount of calls possible. - inputs = [] - for x in entity: - if isinstance(x, str): - inputs.append(x) + return results else: - inputs.append(await self.get_input_entity(x)) + result = await future + self.session.process_entities(result) + self._entity_cache.add(result) + return result + except (errors.ServerError, errors.RpcCallFailError, + errors.RpcMcgetFailError, errors.InterdcCallErrorError, + errors.InterdcCallRichErrorError) as e: + last_error = e + self._log[__name__].warning( + 'Telegram is having internal issues %s: %s', + e.__class__.__name__, e) - lists = { - helpers._EntityType.USER: [], - helpers._EntityType.CHAT: [], - helpers._EntityType.CHANNEL: [], - } - for x in inputs: - try: - lists[helpers._entity_type(x)].append(x) - except TypeError: - pass + await asyncio.sleep(2) + except (errors.FloodWaitError, errors.SlowModeWaitError, errors.FloodTestPhoneWaitError) as e: + last_error = e + if utils.is_list_like(request): + request = request[request_index] - users = lists[helpers._EntityType.USER] - chats = lists[helpers._EntityType.CHAT] - channels = lists[helpers._EntityType.CHANNEL] - if users: - # GetUsersRequest has a limit of 200 per call - tmp = [] - while users: - curr, users = users[:200], users[200:] - tmp.extend(await self(functions.users.GetUsersRequest(curr))) - users = tmp - if chats: # TODO Handle chats slice? - chats = (await self( - functions.messages.GetChatsRequest([x.chat_id for x in chats]))).chats - if channels: - channels = (await self( - functions.channels.GetChannelsRequest(channels))).chats + # SLOW_MODE_WAIT is chat-specific, not request-specific + if not isinstance(e, errors.SlowModeWaitError): + self._flood_waited_requests\ + [request.CONSTRUCTOR_ID] = time.time() + e.seconds - # Merge users, chats and channels into a single dictionary - id_entity = { - utils.get_peer_id(x): x - for x in itertools.chain(users, chats, channels) - } + # In test servers, FLOOD_WAIT_0 has been observed, and sleeping for + # such a short amount will cause retries very fast leading to issues. + if e.seconds == 0: + e.seconds = 1 - # We could check saved usernames and put them into the users, - # chats and channels list from before. While this would reduce - # the amount of ResolveUsername calls, it would fail to catch - # username changes. - result = [] - for x in inputs: - if isinstance(x, str): - result.append(await self._get_entity_from_string(x)) - elif not isinstance(x, types.InputPeerSelf): - result.append(id_entity[utils.get_peer_id(x)]) + if e.seconds <= self.flood_sleep_threshold: + self._log[__name__].info(*_fmt_flood(e.seconds, request)) + await asyncio.sleep(e.seconds) else: - result.append(next( - u for u in id_entity.values() - if isinstance(u, types.User) and u.is_self - )) + raise + except (errors.PhoneMigrateError, errors.NetworkMigrateError, + errors.UserMigrateError) as e: + last_error = e + self._log[__name__].info('Phone migrated to %d', e.new_dc) + should_raise = isinstance(e, ( + errors.PhoneMigrateError, errors.NetworkMigrateError + )) + if should_raise and await self.is_user_authorized(): + raise + await self._switch_dc(e.new_dc) - return result[0] if single else result + if self._raise_last_call_error and last_error is not None: + raise last_error + raise ValueError('Request was unsuccessful {} time(s)' + .format(attempt)) - async def get_input_entity( - self: 'TelegramClient', - peer: 'hints.EntityLike') -> 'types.TypeInputPeer': - """ - Turns the given entity into its input entity version. - Most requests use this kind of :tl:`InputPeer`, so this is the most - suitable call to make for those cases. **Generally you should let the - library do its job** and don't worry about getting the input entity - first, but if you're going to use an entity often, consider making the - call: +async def get_me(self: 'TelegramClient', input_peer: bool = False) \ + -> 'typing.Union[types.User, types.InputPeerUser]': + """ + Gets "me", the current :tl:`User` who is logged in. - Arguments - entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`): - If a username or invite link is given, **the library will - use the cache**. This means that it's possible to be using - a username that *changed* or an old invite link (this only - happens if an invite link for a small group chat is used - after it was upgraded to a mega-group). + If the user has not logged in yet, this method returns `None`. - If the username or ID from the invite link is not found in - the cache, it will be fetched. The same rules apply to phone - numbers (``'+34 123456789'``) from people in your contact list. + Arguments + input_peer (`bool`, optional): + Whether to return the :tl:`InputPeerUser` version or the normal + :tl:`User`. This can be useful if you just need to know the ID + of yourself. - If an exact name is given, it must be in the cache too. This - is not reliable as different people can share the same name - and which entity is returned is arbitrary, and should be used - only for quick tests. + Returns + Your own :tl:`User`. - If a positive integer ID is given, the entity will be searched - in cached users, chats or channels, without making any call. + Example + .. code-block:: python - If a negative integer ID is given, the entity will be searched - exactly as either a chat (prefixed with ``-``) or as a channel - (prefixed with ``-100``). + me = await client.get_me() + print(me.username) + """ + if input_peer and self._self_input_peer: + return self._self_input_peer - If a :tl:`Peer` is given, it will be searched exactly in the - cache as either a user, chat or channel. + try: + me = (await self( + functions.users.GetUsersRequest([types.InputUserSelf()])))[0] - If the given object can be turned into an input entity directly, - said operation will be done. + self._bot = me.bot + if not self._self_input_peer: + self._self_input_peer = utils.get_input_peer( + me, allow_self=False + ) - Unsupported types will raise ``TypeError``. + return self._self_input_peer if input_peer else me + except errors.UnauthorizedError: + return None - If the entity can't be found, ``ValueError`` will be raised. +def _self_id(self: 'TelegramClient') -> typing.Optional[int]: + """ + Returns the ID of the logged-in user, if known. - Returns - :tl:`InputPeerUser`, :tl:`InputPeerChat` or :tl:`InputPeerChannel` - or :tl:`InputPeerSelf` if the parameter is ``'me'`` or ``'self'``. + This property is used in every update, and some like `updateLoginToken` + occur prior to login, so it gracefully handles when no ID is known yet. + """ + return self._self_input_peer.user_id if self._self_input_peer else None - If you need to get the ID of yourself, you should use - `get_me` with ``input_peer=True``) instead. +async def is_bot(self: 'TelegramClient') -> bool: + """ + Return `True` if the signed-in user is a bot, `False` otherwise. - Example - .. code-block:: python + Example + .. code-block:: python - # If you're going to use "username" often in your code - # (make a lot of calls), consider getting its input entity - # once, and then using the "user" everywhere instead. - user = await client.get_input_entity('username') + if await client.is_bot(): + print('Beep') + else: + print('Hello') + """ + if self._bot is None: + self._bot = (await self.get_me()).bot - # The same applies to IDs, chats or channels. - chat = await client.get_input_entity(-123456789) - """ - # Short-circuit if the input parameter directly maps to an InputPeer + return self._bot + +async def is_user_authorized(self: 'TelegramClient') -> bool: + """ + Returns `True` if the user is authorized (logged in). + + Example + .. code-block:: python + + if not await client.is_user_authorized(): + await client.send_code_request(phone) + code = input('enter code: ') + await client.sign_in(phone, code) + """ + if self._authorized is None: try: - return utils.get_input_peer(peer) + # Any request that requires authorization will work + await self(functions.updates.GetStateRequest()) + self._authorized = True + except errors.RPCError: + self._authorized = False + + return self._authorized + +async def get_entity( + self: 'TelegramClient', + entity: 'hints.EntitiesLike') -> 'hints.Entity': + """ + Turns the given entity into a valid Telegram :tl:`User`, :tl:`Chat` + or :tl:`Channel`. You can also pass a list or iterable of entities, + and they will be efficiently fetched from the network. + + Arguments + entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`): + If a username is given, **the username will be resolved** making + an API call every time. Resolving usernames is an expensive + operation and will start hitting flood waits around 50 usernames + in a short period of time. + + If you want to get the entity for a *cached* username, you should + first `get_input_entity(username) ` which will + use the cache), and then use `get_entity` with the result of the + previous call. + + Similar limits apply to invite links, and you should use their + ID instead. + + Using phone numbers (from people in your contact list), exact + names, integer IDs or :tl:`Peer` rely on a `get_input_entity` + first, which in turn needs the entity to be in cache, unless + a :tl:`InputPeer` was passed. + + Unsupported types will raise ``TypeError``. + + If the entity can't be found, ``ValueError`` will be raised. + + Returns + :tl:`User`, :tl:`Chat` or :tl:`Channel` corresponding to the + input entity. A list will be returned if more than one was given. + + Example + .. code-block:: python + + from telethon import utils + + me = await client.get_entity('me') + print(utils.get_display_name(me)) + + chat = await client.get_input_entity('username') + async for message in client.iter_messages(chat): + ... + + # Note that you could have used the username directly, but it's + # good to use get_input_entity if you will reuse it a lot. + async for message in client.iter_messages('username'): + ... + + # Note that for this to work the phone number must be in your contacts + some_id = await client.get_peer_id('+34123456789') + """ + single = not utils.is_list_like(entity) + if single: + entity = (entity,) + + # Group input entities by string (resolve username), + # input users (get users), input chat (get chats) and + # input channels (get channels) to get the most entities + # in the less amount of calls possible. + inputs = [] + for x in entity: + if isinstance(x, str): + inputs.append(x) + else: + inputs.append(await self.get_input_entity(x)) + + lists = { + helpers._EntityType.USER: [], + helpers._EntityType.CHAT: [], + helpers._EntityType.CHANNEL: [], + } + for x in inputs: + try: + lists[helpers._entity_type(x)].append(x) except TypeError: pass - # Next in priority is having a peer (or its ID) cached in-memory + users = lists[helpers._EntityType.USER] + chats = lists[helpers._EntityType.CHAT] + channels = lists[helpers._EntityType.CHANNEL] + if users: + # GetUsersRequest has a limit of 200 per call + tmp = [] + while users: + curr, users = users[:200], users[200:] + tmp.extend(await self(functions.users.GetUsersRequest(curr))) + users = tmp + if chats: # TODO Handle chats slice? + chats = (await self( + functions.messages.GetChatsRequest([x.chat_id for x in chats]))).chats + if channels: + channels = (await self( + functions.channels.GetChannelsRequest(channels))).chats + + # Merge users, chats and channels into a single dictionary + id_entity = { + utils.get_peer_id(x): x + for x in itertools.chain(users, chats, channels) + } + + # We could check saved usernames and put them into the users, + # chats and channels list from before. While this would reduce + # the amount of ResolveUsername calls, it would fail to catch + # username changes. + result = [] + for x in inputs: + if isinstance(x, str): + result.append(await self._get_entity_from_string(x)) + elif not isinstance(x, types.InputPeerSelf): + result.append(id_entity[utils.get_peer_id(x)]) + else: + result.append(next( + u for u in id_entity.values() + if isinstance(u, types.User) and u.is_self + )) + + return result[0] if single else result + +async def get_input_entity( + self: 'TelegramClient', + peer: 'hints.EntityLike') -> 'types.TypeInputPeer': + """ + Turns the given entity into its input entity version. + + Most requests use this kind of :tl:`InputPeer`, so this is the most + suitable call to make for those cases. **Generally you should let the + library do its job** and don't worry about getting the input entity + first, but if you're going to use an entity often, consider making the + call: + + Arguments + entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`): + If a username or invite link is given, **the library will + use the cache**. This means that it's possible to be using + a username that *changed* or an old invite link (this only + happens if an invite link for a small group chat is used + after it was upgraded to a mega-group). + + If the username or ID from the invite link is not found in + the cache, it will be fetched. The same rules apply to phone + numbers (``'+34 123456789'``) from people in your contact list. + + If an exact name is given, it must be in the cache too. This + is not reliable as different people can share the same name + and which entity is returned is arbitrary, and should be used + only for quick tests. + + If a positive integer ID is given, the entity will be searched + in cached users, chats or channels, without making any call. + + If a negative integer ID is given, the entity will be searched + exactly as either a chat (prefixed with ``-``) or as a channel + (prefixed with ``-100``). + + If a :tl:`Peer` is given, it will be searched exactly in the + cache as either a user, chat or channel. + + If the given object can be turned into an input entity directly, + said operation will be done. + + Unsupported types will raise ``TypeError``. + + If the entity can't be found, ``ValueError`` will be raised. + + Returns + :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. + + Example + .. code-block:: python + + # If you're going to use "username" often in your code + # (make a lot of calls), consider getting its input entity + # once, and then using the "user" everywhere instead. + user = await client.get_input_entity('username') + + # The same applies to IDs, chats or channels. + chat = await client.get_input_entity(-123456789) + """ + # Short-circuit if the input parameter directly maps to an InputPeer + try: + return utils.get_input_peer(peer) + except TypeError: + pass + + # Next in priority is having a peer (or its ID) cached in-memory + try: + # 0x2d45687 == crc32(b'Peer') + if isinstance(peer, int) or peer.SUBCLASS_OF_ID == 0x2d45687: + return self._entity_cache[peer] + except (AttributeError, KeyError): + pass + + # Then come known strings that take precedence + if peer in ('me', 'self'): + return types.InputPeerSelf() + + # No InputPeer, cached peer, or known string. Fetch from disk cache + try: + return self.session.get_input_entity(peer) + except ValueError: + pass + + # Only network left to try + if isinstance(peer, str): + return utils.get_input_peer( + await self._get_entity_from_string(peer)) + + # If we're a bot and the user has messaged us privately users.getUsers + # will work with access_hash = 0. Similar for channels.getChannels. + # If we're not a bot but the user is in our contacts, it seems to work + # regardless. These are the only two special-cased requests. + peer = utils.get_peer(peer) + if isinstance(peer, types.PeerUser): + users = await self(functions.users.GetUsersRequest([ + types.InputUser(peer.user_id, access_hash=0)])) + if users and not isinstance(users[0], types.UserEmpty): + # If the user passed a valid ID they expect to work for + # channels but would be valid for users, we get UserEmpty. + # Avoid returning the invalid empty input peer for that. + # + # We *could* try to guess if it's a channel first, and if + # it's not, work as a chat and try to validate it through + # another request, but that becomes too much work. + return utils.get_input_peer(users[0]) + elif isinstance(peer, types.PeerChat): + return types.InputPeerChat(peer.chat_id) + elif isinstance(peer, types.PeerChannel): try: - # 0x2d45687 == crc32(b'Peer') - if isinstance(peer, int) or peer.SUBCLASS_OF_ID == 0x2d45687: - return self._entity_cache[peer] - except (AttributeError, KeyError): + channels = await self(functions.channels.GetChannelsRequest([ + types.InputChannel(peer.channel_id, access_hash=0)])) + return utils.get_input_peer(channels.chats[0]) + except errors.ChannelInvalidError: pass - # Then come known strings that take precedence - if peer in ('me', 'self'): - return types.InputPeerSelf() + raise ValueError( + 'Could not find the input entity for {} ({}). Please read https://' + 'docs.telethon.dev/en/latest/concepts/entities.html to' + ' find out more details.' + .format(peer, type(peer).__name__) + ) - # No InputPeer, cached peer, or known string. Fetch from disk cache +async def _get_peer(self: 'TelegramClient', peer: 'hints.EntityLike'): + i, cls = utils.resolve_id(await self.get_peer_id(peer)) + return cls(i) + +async def get_peer_id( + self: 'TelegramClient', + peer: 'hints.EntityLike', + add_mark: bool = True) -> int: + """ + Gets the ID for the given entity. + + This method needs to be ``async`` because `peer` supports usernames, + invite-links, phone numbers (from people in your contact list), etc. + + If ``add_mark is False``, then a positive ID will be returned + instead. By default, bot-API style IDs (signed) are returned. + + Example + .. code-block:: python + + print(await client.get_peer_id('me')) + """ + if isinstance(peer, int): + return utils.get_peer_id(peer, add_mark=add_mark) + + try: + if peer.SUBCLASS_OF_ID not in (0x2d45687, 0xc91c90b6): + # 0x2d45687, 0xc91c90b6 == crc32(b'Peer') and b'InputPeer' + peer = await self.get_input_entity(peer) + except AttributeError: + peer = await self.get_input_entity(peer) + + if isinstance(peer, types.InputPeerSelf): + peer = await self.get_me(input_peer=True) + + return utils.get_peer_id(peer, add_mark=add_mark) + + +async def _get_entity_from_string(self: 'TelegramClient', string): + """ + Gets a full entity from the given string, which may be a phone or + a username, and processes all the found entities on the session. + The string may also be a user link, or a channel/chat invite link. + + This method has the side effect of adding the found users to the + session database, so it can be queried later without API calls, + if this option is enabled on the session. + + Returns the found entity, or raises TypeError if not found. + """ + phone = utils.parse_phone(string) + if phone: try: - return self.session.get_input_entity(peer) + for user in (await self( + functions.contacts.GetContactsRequest(0))).users: + if user.phone == phone: + return user + except errors.BotMethodInvalidError: + raise ValueError('Cannot get entity by phone number as a ' + 'bot (try using integer IDs, not strings)') + elif string.lower() in ('me', 'self'): + return await self.get_me() + else: + username, is_join_chat = utils.parse_username(string) + if is_join_chat: + invite = await self( + functions.messages.CheckChatInviteRequest(username)) + + if isinstance(invite, types.ChatInvite): + raise ValueError( + 'Cannot get entity from a channel (or group) ' + 'that you are not part of. Join the group and retry' + ) + elif isinstance(invite, types.ChatInviteAlready): + return invite.chat + elif username: + try: + result = await self( + functions.contacts.ResolveUsernameRequest(username)) + except errors.UsernameNotOccupiedError as e: + raise ValueError('No user has "{}" as username' + .format(username)) from e + + try: + pid = utils.get_peer_id(result.peer, add_mark=False) + if isinstance(result.peer, types.PeerUser): + return next(x for x in result.users if x.id == pid) + else: + return next(x for x in result.chats if x.id == pid) + except StopIteration: + pass + try: + # Nobody with this username, maybe it's an exact name/title + return await self.get_entity( + self.session.get_input_entity(string)) except ValueError: pass - # Only network left to try - if isinstance(peer, str): - return utils.get_input_peer( - await self._get_entity_from_string(peer)) + raise ValueError( + 'Cannot find any entity corresponding to "{}"'.format(string) + ) - # If we're a bot and the user has messaged us privately users.getUsers - # will work with access_hash = 0. Similar for channels.getChannels. - # If we're not a bot but the user is in our contacts, it seems to work - # regardless. These are the only two special-cased requests. - peer = utils.get_peer(peer) - if isinstance(peer, types.PeerUser): - users = await self(functions.users.GetUsersRequest([ - types.InputUser(peer.user_id, access_hash=0)])) - if users and not isinstance(users[0], types.UserEmpty): - # If the user passed a valid ID they expect to work for - # channels but would be valid for users, we get UserEmpty. - # Avoid returning the invalid empty input peer for that. - # - # We *could* try to guess if it's a channel first, and if - # it's not, work as a chat and try to validate it through - # another request, but that becomes too much work. - return utils.get_input_peer(users[0]) - elif isinstance(peer, types.PeerChat): - return types.InputPeerChat(peer.chat_id) - elif isinstance(peer, types.PeerChannel): - try: - channels = await self(functions.channels.GetChannelsRequest([ - types.InputChannel(peer.channel_id, access_hash=0)])) - return utils.get_input_peer(channels.chats[0]) - except errors.ChannelInvalidError: - pass +async def _get_input_dialog(self: 'TelegramClient', dialog): + """ + Returns a :tl:`InputDialogPeer`. This is a bit tricky because + it may or not need access to the client to convert what's given + into an input entity. + """ + try: + if dialog.SUBCLASS_OF_ID == 0xa21c9795: # crc32(b'InputDialogPeer') + dialog.peer = await self.get_input_entity(dialog.peer) + return dialog + elif dialog.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer') + return types.InputDialogPeer(dialog) + except AttributeError: + pass - raise ValueError( - 'Could not find the input entity for {} ({}). Please read https://' - 'docs.telethon.dev/en/latest/concepts/entities.html to' - ' find out more details.' - .format(peer, type(peer).__name__) - ) + return types.InputDialogPeer(await self.get_input_entity(dialog)) - async def _get_peer(self: 'TelegramClient', peer: 'hints.EntityLike'): - i, cls = utils.resolve_id(await self.get_peer_id(peer)) - return cls(i) +async def _get_input_notify(self: 'TelegramClient', notify): + """ + Returns a :tl:`InputNotifyPeer`. This is a bit tricky because + it may or not need access to the client to convert what's given + into an input entity. + """ + try: + if notify.SUBCLASS_OF_ID == 0x58981615: + if isinstance(notify, types.InputNotifyPeer): + notify.peer = await self.get_input_entity(notify.peer) + return notify + except AttributeError: + pass - async def get_peer_id( - self: 'TelegramClient', - peer: 'hints.EntityLike', - add_mark: bool = True) -> int: - """ - Gets the ID for the given entity. - - This method needs to be ``async`` because `peer` supports usernames, - invite-links, phone numbers (from people in your contact list), etc. - - If ``add_mark is False``, then a positive ID will be returned - instead. By default, bot-API style IDs (signed) are returned. - - Example - .. code-block:: python - - print(await client.get_peer_id('me')) - """ - if isinstance(peer, int): - return utils.get_peer_id(peer, add_mark=add_mark) - - try: - if peer.SUBCLASS_OF_ID not in (0x2d45687, 0xc91c90b6): - # 0x2d45687, 0xc91c90b6 == crc32(b'Peer') and b'InputPeer' - peer = await self.get_input_entity(peer) - except AttributeError: - peer = await self.get_input_entity(peer) - - if isinstance(peer, types.InputPeerSelf): - peer = await self.get_me(input_peer=True) - - return utils.get_peer_id(peer, add_mark=add_mark) - - # endregion - - # region Private methods - - async def _get_entity_from_string(self: 'TelegramClient', string): - """ - Gets a full entity from the given string, which may be a phone or - a username, and processes all the found entities on the session. - The string may also be a user link, or a channel/chat invite link. - - This method has the side effect of adding the found users to the - session database, so it can be queried later without API calls, - if this option is enabled on the session. - - Returns the found entity, or raises TypeError if not found. - """ - phone = utils.parse_phone(string) - if phone: - try: - for user in (await self( - functions.contacts.GetContactsRequest(0))).users: - if user.phone == phone: - return user - except errors.BotMethodInvalidError: - raise ValueError('Cannot get entity by phone number as a ' - 'bot (try using integer IDs, not strings)') - elif string.lower() in ('me', 'self'): - return await self.get_me() - else: - username, is_join_chat = utils.parse_username(string) - if is_join_chat: - invite = await self( - functions.messages.CheckChatInviteRequest(username)) - - if isinstance(invite, types.ChatInvite): - raise ValueError( - 'Cannot get entity from a channel (or group) ' - 'that you are not part of. Join the group and retry' - ) - elif isinstance(invite, types.ChatInviteAlready): - return invite.chat - elif username: - try: - result = await self( - functions.contacts.ResolveUsernameRequest(username)) - except errors.UsernameNotOccupiedError as e: - raise ValueError('No user has "{}" as username' - .format(username)) from e - - try: - pid = utils.get_peer_id(result.peer, add_mark=False) - if isinstance(result.peer, types.PeerUser): - return next(x for x in result.users if x.id == pid) - else: - return next(x for x in result.chats if x.id == pid) - except StopIteration: - pass - try: - # Nobody with this username, maybe it's an exact name/title - return await self.get_entity( - self.session.get_input_entity(string)) - except ValueError: - pass - - raise ValueError( - 'Cannot find any entity corresponding to "{}"'.format(string) - ) - - async def _get_input_dialog(self: 'TelegramClient', dialog): - """ - Returns a :tl:`InputDialogPeer`. This is a bit tricky because - it may or not need access to the client to convert what's given - into an input entity. - """ - try: - if dialog.SUBCLASS_OF_ID == 0xa21c9795: # crc32(b'InputDialogPeer') - dialog.peer = await self.get_input_entity(dialog.peer) - return dialog - elif dialog.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer') - return types.InputDialogPeer(dialog) - except AttributeError: - pass - - return types.InputDialogPeer(await self.get_input_entity(dialog)) - - async def _get_input_notify(self: 'TelegramClient', notify): - """ - Returns a :tl:`InputNotifyPeer`. This is a bit tricky because - it may or not need access to the client to convert what's given - into an input entity. - """ - try: - if notify.SUBCLASS_OF_ID == 0x58981615: - if isinstance(notify, types.InputNotifyPeer): - notify.peer = await self.get_input_entity(notify.peer) - return notify - except AttributeError: - pass - - return types.InputNotifyPeer(await self.get_input_entity(notify)) - - # endregion + return types.InputNotifyPeer(await self.get_input_entity(notify)) From d6326abacb43496958129101343c06930c5e691e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 11 Sep 2021 13:35:35 +0200 Subject: [PATCH 034/256] Rename client module as _client --- readthedocs/misc/v2-migration-guide.rst | 5 ----- telethon/{client => _client}/__init__.py | 0 telethon/{client => _client}/account.py | 0 telethon/{client => _client}/auth.py | 0 telethon/{client => _client}/bots.py | 0 telethon/{client => _client}/buttons.py | 0 telethon/{client => _client}/chats.py | 0 telethon/{client => _client}/dialogs.py | 0 telethon/{client => _client}/downloads.py | 0 telethon/{client => _client}/messageparse.py | 0 telethon/{client => _client}/messages.py | 0 telethon/{client => _client}/telegrambaseclient.py | 0 telethon/{client => _client}/telegramclient.py | 0 telethon/{client => _client}/updates.py | 0 telethon/{client => _client}/uploads.py | 0 telethon/{client => _client}/users.py | 0 16 files changed, 5 deletions(-) rename telethon/{client => _client}/__init__.py (100%) rename telethon/{client => _client}/account.py (100%) rename telethon/{client => _client}/auth.py (100%) rename telethon/{client => _client}/bots.py (100%) rename telethon/{client => _client}/buttons.py (100%) rename telethon/{client => _client}/chats.py (100%) rename telethon/{client => _client}/dialogs.py (100%) rename telethon/{client => _client}/downloads.py (100%) rename telethon/{client => _client}/messageparse.py (100%) rename telethon/{client => _client}/messages.py (100%) rename telethon/{client => _client}/telegrambaseclient.py (100%) rename telethon/{client => _client}/telegramclient.py (100%) rename telethon/{client => _client}/updates.py (100%) rename telethon/{client => _client}/uploads.py (100%) rename telethon/{client => _client}/users.py (100%) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index cd887251..7655e673 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -32,8 +32,3 @@ 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 \ No newline at end of file diff --git a/telethon/client/__init__.py b/telethon/_client/__init__.py similarity index 100% rename from telethon/client/__init__.py rename to telethon/_client/__init__.py diff --git a/telethon/client/account.py b/telethon/_client/account.py similarity index 100% rename from telethon/client/account.py rename to telethon/_client/account.py diff --git a/telethon/client/auth.py b/telethon/_client/auth.py similarity index 100% rename from telethon/client/auth.py rename to telethon/_client/auth.py diff --git a/telethon/client/bots.py b/telethon/_client/bots.py similarity index 100% rename from telethon/client/bots.py rename to telethon/_client/bots.py diff --git a/telethon/client/buttons.py b/telethon/_client/buttons.py similarity index 100% rename from telethon/client/buttons.py rename to telethon/_client/buttons.py diff --git a/telethon/client/chats.py b/telethon/_client/chats.py similarity index 100% rename from telethon/client/chats.py rename to telethon/_client/chats.py diff --git a/telethon/client/dialogs.py b/telethon/_client/dialogs.py similarity index 100% rename from telethon/client/dialogs.py rename to telethon/_client/dialogs.py diff --git a/telethon/client/downloads.py b/telethon/_client/downloads.py similarity index 100% rename from telethon/client/downloads.py rename to telethon/_client/downloads.py diff --git a/telethon/client/messageparse.py b/telethon/_client/messageparse.py similarity index 100% rename from telethon/client/messageparse.py rename to telethon/_client/messageparse.py diff --git a/telethon/client/messages.py b/telethon/_client/messages.py similarity index 100% rename from telethon/client/messages.py rename to telethon/_client/messages.py diff --git a/telethon/client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py similarity index 100% rename from telethon/client/telegrambaseclient.py rename to telethon/_client/telegrambaseclient.py diff --git a/telethon/client/telegramclient.py b/telethon/_client/telegramclient.py similarity index 100% rename from telethon/client/telegramclient.py rename to telethon/_client/telegramclient.py diff --git a/telethon/client/updates.py b/telethon/_client/updates.py similarity index 100% rename from telethon/client/updates.py rename to telethon/_client/updates.py diff --git a/telethon/client/uploads.py b/telethon/_client/uploads.py similarity index 100% rename from telethon/client/uploads.py rename to telethon/_client/uploads.py diff --git a/telethon/client/users.py b/telethon/_client/users.py similarity index 100% rename from telethon/client/users.py rename to telethon/_client/users.py From 34e7b7cc9fd8ea3b9a0179e3de8edc5b63b52e78 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 11 Sep 2021 13:43:20 +0200 Subject: [PATCH 035/256] Fix some import errors --- telethon/_client/__init__.py | 25 ++----------------------- telethon/_client/telegramclient.py | 23 ++++++++++++++--------- 2 files changed, 16 insertions(+), 32 deletions(-) diff --git a/telethon/_client/__init__.py b/telethon/_client/__init__.py index e0463ab0..5cfe7a83 100644 --- a/telethon/_client/__init__.py +++ b/telethon/_client/__init__.py @@ -1,25 +1,4 @@ """ -This package defines clients as subclasses of others, and then a single -`telethon.client.telegramclient.TelegramClient` which is subclass of them -all to provide the final unified interface while the methods can live in -different subclasses to be more maintainable. - -The ABC is `telethon.client.telegrambaseclient.TelegramBaseClient` and the -first implementor is `telethon.client.users.UserMethods`, since calling -requests require them to be resolved first, and that requires accessing -entities (users). +This package defines the main `telethon._client.telegramclient.TelegramClient` instance +which delegates the work to free-standing functions defined in the rest of files. """ -from .telegrambaseclient import TelegramBaseClient -from .users import UserMethods # Required for everything -from .messageparse import MessageParseMethods # Required for messages -from .uploads import UploadMethods # Required for messages to send files -from .updates import UpdateMethods # Required for buttons (register callbacks) -from .buttons import ButtonMethods # Required for messages to use buttons -from .messages import MessageMethods -from .chats import ChatMethods -from .dialogs import DialogMethods -from .downloads import DownloadMethods -from .account import AccountMethods -from .auth import AuthMethods -from .bots import BotMethods -from .telegramclient import TelegramClient diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index f1fbaa82..58f6b56a 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -1,12 +1,17 @@ +import asyncio import functools import inspect import typing +import logging from . import ( account, auth, bots, buttons, chats, dialogs, downloads, messageparse, messages, telegrambaseclient, updates, uploads, users ) -from .. import helpers +from .. import helpers, version +from ..tl import types, custom +from ..network import ConnectionTcpFull +from ..events.common import EventBuilder, EventCommon class TelegramClient: @@ -721,7 +726,7 @@ class TelegramClient: *, search: str = '', filter: 'types.TypeChannelParticipantsFilter' = None, - aggressive: bool = False) -> _ParticipantsIter: + aggressive: bool = False) -> chats._ParticipantsIter: """ Iterator over the participants belonging to the specified chat. @@ -830,7 +835,7 @@ class TelegramClient: pinned: bool = None, edit: bool = None, delete: bool = None, - group_call: bool = None) -> _AdminLogIter: + group_call: bool = None) -> chats._AdminLogIter: """ Iterator over the admin log for the specified channel. @@ -958,7 +963,7 @@ class TelegramClient: limit: int = None, *, offset: int = 0, - max_id: int = 0) -> _ProfilePhotoIter: + max_id: int = 0) -> chats._ProfilePhotoIter: """ Iterator over a user's profile photos or a chat's photos. @@ -1452,7 +1457,7 @@ class TelegramClient: ignore_migrated: bool = False, folder: int = None, archived: bool = None - ) -> _DialogsIter: + ) -> dialogs._DialogsIter: """ Iterator over the dialogs (open conversations/subscribed channels). @@ -1549,7 +1554,7 @@ class TelegramClient: def iter_drafts( self: 'TelegramClient', entity: 'hints.EntitiesLike' = None - ) -> _DraftsIter: + ) -> dialogs._DraftsIter: """ Iterator over draft messages. @@ -2023,7 +2028,7 @@ class TelegramClient: stride: int = None, limit: int = None, chunk_size: int = None, - request_size: int = MAX_CHUNK_SIZE, + request_size: int = downloads.MAX_CHUNK_SIZE, file_size: int = None, dc_id: int = None ): @@ -3180,7 +3185,7 @@ class TelegramClient: def add_event_handler( self: 'TelegramClient', - callback: Callback, + callback: updates.Callback, event: EventBuilder = None): """ Registers a new event handler callback. @@ -3218,7 +3223,7 @@ class TelegramClient: def remove_event_handler( self: 'TelegramClient', - callback: Callback, + callback: updates.Callback, event: EventBuilder = None) -> int: """ Inverse operation of `add_event_handler()`. From 2a933ac3bd02cc36b39e3e967b850c6e4511016d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 11 Sep 2021 14:05:24 +0200 Subject: [PATCH 036/256] Remove sync hack --- .github/ISSUE_TEMPLATE/bug-report.md | 2 +- README.rst | 22 +++--- readthedocs/basic/quick-start.rst | 12 ++-- readthedocs/basic/signing-in.rst | 20 ++++-- readthedocs/concepts/asyncio.rst | 93 +------------------------ readthedocs/concepts/sessions.rst | 8 +-- readthedocs/index.rst | 20 +++--- readthedocs/misc/v2-migration-guide.rst | 19 +++++ readthedocs/quick-references/faq.rst | 23 +----- telethon/_client/account.py | 3 - telethon/_client/auth.py | 8 +-- telethon/_client/chats.py | 3 - telethon/_client/downloads.py | 3 - telethon/_client/telegrambaseclient.py | 15 +--- telethon/_client/telegramclient.py | 3 - telethon/_client/updates.py | 12 +--- telethon/helpers.py | 30 +------- telethon/requestiter.py | 18 ----- telethon/sync.py | 74 -------------------- telethon/tl/custom/conversation.py | 3 - telethon_generator/generators/docs.py | 2 +- tests/telethon/test_helpers.py | 37 ---------- 22 files changed, 77 insertions(+), 353 deletions(-) delete mode 100644 telethon/sync.py diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index dc7a26c2..1e7ebec6 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -14,7 +14,7 @@ assignees: '' **Code that causes the issue** ```python -from telethon.sync import TelegramClient +from telethon import TelegramClient ... ``` diff --git a/README.rst b/README.rst index f1eb902c..15985350 100755 --- a/README.rst +++ b/README.rst @@ -35,15 +35,19 @@ Creating a client .. code-block:: python - from telethon import TelegramClient, events, sync + import asyncio + from telethon import TelegramClient, events # These example values won't work. You must get your own api_id and # api_hash from https://my.telegram.org, under API Development. api_id = 12345 api_hash = '0123456789abcdef0123456789abcdef' - client = TelegramClient('session_name', api_id, api_hash) - client.start() + async def main(): + client = TelegramClient('session_name', api_id, api_hash) + await client.start() + + asyncio.run(main()) Doing stuff @@ -51,14 +55,14 @@ Doing stuff .. code-block:: python - print(client.get_me().stringify()) + print((await client.get_me()).stringify()) - client.send_message('username', 'Hello! Talking to you from Telethon') - client.send_file('username', '/home/myself/Pictures/holidays.jpg') + await client.send_message('username', 'Hello! Talking to you from Telethon') + await client.send_file('username', '/home/myself/Pictures/holidays.jpg') - client.download_profile_photo('me') - messages = client.get_messages('username') - messages[0].download_media() + await client.download_profile_photo('me') + messages = await client.get_messages('username') + await messages[0].download_media() @client.on(events.NewMessage(pattern='(?i)hi|hello')) async def handler(event): diff --git a/readthedocs/basic/quick-start.rst b/readthedocs/basic/quick-start.rst index cd187c81..8dbf928d 100644 --- a/readthedocs/basic/quick-start.rst +++ b/readthedocs/basic/quick-start.rst @@ -100,12 +100,8 @@ proceeding. We will see all the available methods later on. # Most of your code should go here. # You can of course make and use your own async def (do_something). # They only need to be async if they need to await things. - me = await client.get_me() - await do_something(me) + async with client: + me = await client.get_me() + await do_something(me) - with client: - client.loop.run_until_complete(main()) - - After you understand this, you may use the ``telethon.sync`` hack if you - want do so (see :ref:`compatibility-and-convenience`), but note you may - run into other issues (iPython, Anaconda, etc. have some issues with it). + client.loop.run_until_complete(main()) diff --git a/readthedocs/basic/signing-in.rst b/readthedocs/basic/signing-in.rst index 9fb14853..7f584a95 100644 --- a/readthedocs/basic/signing-in.rst +++ b/readthedocs/basic/signing-in.rst @@ -55,9 +55,12 @@ We can finally write some code to log into our account! api_id = 12345 api_hash = '0123456789abcdef0123456789abcdef' - # The first parameter is the .session file name (absolute paths allowed) - with TelegramClient('anon', api_id, api_hash) as client: - client.loop.run_until_complete(client.send_message('me', 'Hello, myself!')) + async def main(): + # The first parameter is the .session file name (absolute paths allowed) + async with TelegramClient('anon', api_id, api_hash) as client: + await client.send_message('me', 'Hello, myself!') + + client.loop.run_until_complete(main()) In the first line, we import the class name so we can create an instance @@ -95,7 +98,7 @@ You will still need an API ID and hash, but the process is very similar: .. code-block:: python - from telethon.sync import TelegramClient + from telethon import TelegramClient api_id = 12345 api_hash = '0123456789abcdef0123456789abcdef' @@ -104,9 +107,12 @@ You will still need an API ID and hash, but the process is very similar: # We have to manually call "start" if we want an explicit bot token bot = TelegramClient('bot', api_id, api_hash).start(bot_token=bot_token) - # But then we can use the client instance as usual - with bot: - ... + async def main(): + # But then we can use the client instance as usual + async with bot: + ... + + client.loop.run_until_complete(main()) To get a bot account, you need to talk diff --git a/readthedocs/concepts/asyncio.rst b/readthedocs/concepts/asyncio.rst index ef7c3cd3..dd85f957 100644 --- a/readthedocs/concepts/asyncio.rst +++ b/readthedocs/concepts/asyncio.rst @@ -58,84 +58,6 @@ What are asyncio basics? loop.run_until_complete(main()) -What does telethon.sync do? -=========================== - -The moment you import any of these: - -.. code-block:: python - - from telethon import sync, ... - # or - from telethon.sync import ... - # or - import telethon.sync - -The ``sync`` module rewrites most ``async def`` -methods in Telethon to something similar to this: - -.. code-block:: python - - def new_method(): - result = original_method() - if loop.is_running(): - # the loop is already running, return the await-able to the user - return result - else: - # the loop is not running yet, so we can run it for the user - return loop.run_until_complete(result) - - -That means you can do this: - -.. code-block:: python - - print(client.get_me().username) - -Instead of this: - -.. code-block:: python - - me = client.loop.run_until_complete(client.get_me()) - print(me.username) - - # or, using asyncio's default loop (it's the same) - import asyncio - loop = asyncio.get_event_loop() # == client.loop - me = loop.run_until_complete(client.get_me()) - print(me.username) - - -As you can see, it's a lot of boilerplate and noise having to type -``run_until_complete`` all the time, so you can let the magic module -to rewrite it for you. But notice the comment above: it won't run -the loop if it's already running, because it can't. That means this: - -.. code-block:: python - - async def main(): - # 3. the loop is running here - print( - client.get_me() # 4. this will return a coroutine! - .username # 5. this fails, coroutines don't have usernames - ) - - loop.run_until_complete( # 2. run the loop and the ``main()`` coroutine - main() # 1. calling ``async def`` "returns" a coroutine - ) - - -Will fail. So if you're inside an ``async def``, then the loop is -running, and if the loop is running, you must ``await`` things yourself: - -.. code-block:: python - - async def main(): - print((await client.get_me()).username) - - loop.run_until_complete(main()) - - What are async, await and coroutines? ===================================== @@ -275,7 +197,7 @@ in it. So if you want to run *other* code, create tasks for it: loop.create_task(clock()) ... - client.run_until_disconnected() + await client.run_until_disconnected() This creates a task for a clock that prints the time every second. You don't need to use `client.run_until_disconnected() @@ -344,19 +266,6 @@ When you use a library, you're not limited to use only its methods. You can combine all the libraries you want. People seem to forget this simple fact! -Why does client.start() work outside async? -=========================================== - -Because it's so common that it's really convenient to offer said -functionality by default. This means you can set up all your event -handlers and start the client without worrying about loops at all. - -Using the client in a ``with`` block, `start -`, `run_until_disconnected -`, and -`disconnect ` -all support this. - Where can I read more? ====================== diff --git a/readthedocs/concepts/sessions.rst b/readthedocs/concepts/sessions.rst index a94bc773..8ba75938 100644 --- a/readthedocs/concepts/sessions.rst +++ b/readthedocs/concepts/sessions.rst @@ -73,10 +73,10 @@ You can import these ``from telethon.sessions``. For example, using the .. code-block:: python - from telethon.sync import TelegramClient + from telethon import TelegramClient from telethon.sessions import StringSession - with TelegramClient(StringSession(string), api_id, api_hash) as client: + async with TelegramClient(StringSession(string), api_id, api_hash) as client: ... # use the client # Save the string session as a string; you should decide how @@ -129,10 +129,10 @@ The easiest way to generate a string session is as follows: .. code-block:: python - from telethon.sync import TelegramClient + from telethon import TelegramClient from telethon.sessions import StringSession - with TelegramClient(StringSession(), api_id, api_hash) as client: + async with TelegramClient(StringSession(), api_id, api_hash) as client: print(client.session.save()) diff --git a/readthedocs/index.rst b/readthedocs/index.rst index 827823cd..1794ce72 100644 --- a/readthedocs/index.rst +++ b/readthedocs/index.rst @@ -4,17 +4,21 @@ Telethon's Documentation .. code-block:: python - from telethon.sync import TelegramClient, events + import asyncio + from telethon import TelegramClient, events - with TelegramClient('name', api_id, api_hash) as client: - client.send_message('me', 'Hello, myself!') - print(client.download_profile_photo('me')) + async def main(): + async with TelegramClient('name', api_id, api_hash) as client: + await client.send_message('me', 'Hello, myself!') + print(await client.download_profile_photo('me')) - @client.on(events.NewMessage(pattern='(?i).*Hello')) - async def handler(event): - await event.reply('Hey!') + @client.on(events.NewMessage(pattern='(?i).*Hello')) + async def handler(event): + await event.reply('Hey!') - client.run_until_disconnected() + await client.run_until_disconnected() + + asyncio.run(main()) * Are you new here? Jump straight into :ref:`installation`! diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 7655e673..5c748895 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -32,3 +32,22 @@ 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``). + + +Synchronous compatibility mode has been removed +----------------------------------------------- + +The "sync hack" (which kicked in as soon as anything from ``telethon.sync`` was imported) has been +removed. This implies: + +* The ``telethon.sync`` module is gone. +* Synchronous context-managers (``with`` as opposed to ``async with``) are no longer supported. + Most notably, you can no longer do ``with client``. It must be ``async with client`` now. +* The "smart" behaviour of the following methods has been removed and now they no longer work in + a synchronous context when the ``asyncio`` event loop was not running. This means they now need + to be used with ``await`` (or, alternatively, manually used with ``loop.run_until_complete``): + * ``start`` + * ``disconnect`` + * ``run_until_disconnected`` + +// TODO provide standalone alternative for this? diff --git a/readthedocs/quick-references/faq.rst b/readthedocs/quick-references/faq.rst index df267b86..0b1e28b0 100644 --- a/readthedocs/quick-references/faq.rst +++ b/readthedocs/quick-references/faq.rst @@ -127,14 +127,7 @@ This is basic Python knowledge. You should use the dot operator: AttributeError: 'coroutine' object has no attribute 'id' ======================================================== -You either forgot to: - -.. code-block:: python - - import telethon.sync - # ^^^^^ import sync - -Or: +Telethon is an asynchronous library. This means you need to ``await`` most methods: .. code-block:: python @@ -218,19 +211,7 @@ Check out `quart_login.py`_ for an example web-application based on Quart. Can I use Anaconda/Spyder/IPython with the library? =================================================== -Yes, but these interpreters run the asyncio event loop implicitly, -which interferes with the ``telethon.sync`` magic module. - -If you use them, you should **not** import ``sync``: - -.. code-block:: python - - # Change any of these...: - from telethon import TelegramClient, sync, ... - from telethon.sync import TelegramClient, ... - - # ...with this: - from telethon import TelegramClient, ... +Yes, but these interpreters run the asyncio event loop implicitly, so be wary of that. You are also more likely to get "sqlite3.OperationalError: database is locked" with them. If they cause too much trouble, just write your code in a ``.py`` diff --git a/telethon/_client/account.py b/telethon/_client/account.py index 6300b791..46e0b6dc 100644 --- a/telethon/_client/account.py +++ b/telethon/_client/account.py @@ -56,9 +56,6 @@ class _TakeoutClient: raise ValueError("Failed to finish the takeout.") self.session.takeout_id = None - __enter__ = helpers._sync_enter - __exit__ = helpers._sync_exit - async def __call__(self, request, ordered=False): takeout_id = self.__client.session.takeout_id if takeout_id is None: diff --git a/telethon/_client/auth.py b/telethon/_client/auth.py index c27f6512..3699d795 100644 --- a/telethon/_client/auth.py +++ b/telethon/_client/auth.py @@ -12,7 +12,7 @@ if typing.TYPE_CHECKING: from .telegramclient import TelegramClient -def start( +async def start( self: 'TelegramClient', phone: typing.Callable[[], str] = lambda: input('Please enter your phone (or bot token): '), password: typing.Callable[[], str] = lambda: getpass.getpass('Please enter your password: '), @@ -39,7 +39,7 @@ def start( raise ValueError('Both a phone and a bot token provided, ' 'must only provide one of either') - coro = self._start( + return await self._start( phone=phone, password=password, bot_token=bot_token, @@ -49,10 +49,6 @@ def start( last_name=last_name, max_attempts=max_attempts ) - return ( - coro if self.loop.is_running() - else self.loop.run_until_complete(coro) - ) async def _start( self: 'TelegramClient', phone, password, bot_token, force_sms, diff --git a/telethon/_client/chats.py b/telethon/_client/chats.py index 0429d563..4147b45b 100644 --- a/telethon/_client/chats.py +++ b/telethon/_client/chats.py @@ -76,9 +76,6 @@ class _ChatAction: self._task = None - __enter__ = helpers._sync_enter - __exit__ = helpers._sync_exit - async def _update(self): try: while self._running: diff --git a/telethon/_client/downloads.py b/telethon/_client/downloads.py index 2150dc92..4500df33 100644 --- a/telethon/_client/downloads.py +++ b/telethon/_client/downloads.py @@ -137,9 +137,6 @@ class _DirectDownloadIter(RequestIter): async def __aexit__(self, *args): await self.close() - __enter__ = helpers._sync_enter - __exit__ = helpers._sync_exit - class _GenericDownloadIter(_DirectDownloadIter): async def _load_next_chunk(self): diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 79ea85b3..16822d6a 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -337,19 +337,8 @@ def is_connected(self: 'TelegramClient') -> bool: sender = getattr(self, '_sender', None) return sender and sender.is_connected() -def disconnect(self: 'TelegramClient'): - if self.loop.is_running(): - return self._disconnect_coro() - else: - try: - self.loop.run_until_complete(self._disconnect_coro()) - except RuntimeError: - # Python 3.5.x complains when called from - # `__aexit__` and there were pending updates with: - # "Event loop stopped before Future completed." - # - # However, it doesn't really make a lot of sense. - pass +async def disconnect(self: 'TelegramClient'): + return await self._disconnect_coro() def set_proxy(self: 'TelegramClient', proxy: typing.Union[tuple, dict]): init_proxy = None if not issubclass(self._connection, TcpMTProxy) else \ diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 58f6b56a..1c50a805 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -619,9 +619,6 @@ class TelegramClient: async def __aexit__(self, *args): await self.disconnect() - __enter__ = helpers._sync_enter - __exit__ = helpers._sync_exit - # endregion Auth # region Bots diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index 4860a8cd..04d7fbfb 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -40,7 +40,7 @@ async def set_receive_updates(self: 'TelegramClient', receive_updates): if receive_updates: await self(functions.updates.GetStateRequest()) -def run_until_disconnected(self: 'TelegramClient'): +async def run_until_disconnected(self: 'TelegramClient'): """ Runs the event loop until the library is disconnected. @@ -75,15 +75,7 @@ def run_until_disconnected(self: 'TelegramClient'): # script from exiting. await client.run_until_disconnected() """ - if self.loop.is_running(): - return self._run_until_disconnected() - try: - return self.loop.run_until_complete(self._run_until_disconnected()) - except KeyboardInterrupt: - pass - finally: - # No loop.run_until_complete; it's already syncified - self.disconnect() + return await self._run_until_disconnected() def on(self: 'TelegramClient', event: EventBuilder): """ diff --git a/telethon/helpers.py b/telethon/helpers.py index 6c782b0b..f9297816 100644 --- a/telethon/helpers.py +++ b/telethon/helpers.py @@ -118,7 +118,7 @@ def retry_range(retries, force_retry=True): while attempt != retries: attempt += 1 yield attempt - + async def _maybe_await(value): @@ -165,34 +165,6 @@ async def _cancel(log, **tasks): '%s (%s)', name, type(task), task) -def _sync_enter(self): - """ - Helps to cut boilerplate on async context - managers that offer synchronous variants. - """ - if hasattr(self, 'loop'): - loop = self.loop - else: - loop = self._client.loop - - if loop.is_running(): - raise RuntimeError( - 'You must use "async with" if the event loop ' - 'is running (i.e. you are inside an "async def")' - ) - - return loop.run_until_complete(self.__aenter__()) - - -def _sync_exit(self, *args): - if hasattr(self, 'loop'): - loop = self.loop - else: - loop = self._client.loop - - return loop.run_until_complete(self.__aexit__(*args)) - - def _entity_type(entity): # This could be a `utils` method that just ran a few `isinstance` on # `utils.get_peer(...)`'s result. However, there are *a lot* of auto diff --git a/telethon/requestiter.py b/telethon/requestiter.py index fd28419d..6473fe0f 100644 --- a/telethon/requestiter.py +++ b/telethon/requestiter.py @@ -12,9 +12,6 @@ class RequestIter(abc.ABC): It has some facilities, such as automatically sleeping a desired amount of time between requests if needed (but not more). - Can be used synchronously if the event loop is not running and - as an asynchronous iterator otherwise. - `limit` is the total amount of items that the iterator should return. This is handled on this base class, and will be always ``>= 0``. @@ -82,12 +79,6 @@ class RequestIter(abc.ABC): self.index += 1 return result - def __next__(self): - try: - return self.client.loop.run_until_complete(self.__anext__()) - except StopAsyncIteration: - raise StopIteration - def __aiter__(self): self.buffer = None self.index = 0 @@ -95,15 +86,6 @@ class RequestIter(abc.ABC): self.left = self.limit return self - def __iter__(self): - if self.client.loop.is_running(): - raise RuntimeError( - 'You must use "async for" if the event loop ' - 'is running (i.e. you are inside an "async def")' - ) - - return self.__aiter__() - async def collect(self): """ Create a `self` iterator and collect it into a `TotalList` diff --git a/telethon/sync.py b/telethon/sync.py deleted file mode 100644 index 80b80bea..00000000 --- a/telethon/sync.py +++ /dev/null @@ -1,74 +0,0 @@ -""" -This magical module will rewrite all public methods in the public interface -of the library so they can run the loop on their own if it's not already -running. This rewrite may not be desirable if the end user always uses the -methods they way they should be ran, but it's incredibly useful for quick -scripts and the runtime overhead is relatively low. - -Some really common methods which are hardly used offer this ability by -default, such as ``.start()`` and ``.run_until_disconnected()`` (since -you may want to start, and then run until disconnected while using async -event handlers). -""" -import asyncio -import functools -import inspect - -from . import events, errors, utils, connection -from .client.account import _TakeoutClient -from .client.telegramclient import TelegramClient -from .tl import types, functions, custom -from .tl.custom import ( - Draft, Dialog, MessageButton, Forward, Button, - Message, InlineResult, Conversation -) -from .tl.custom.chatgetter import ChatGetter -from .tl.custom.sendergetter import SenderGetter - - -def _syncify_wrap(t, method_name): - method = getattr(t, method_name) - - @functools.wraps(method) - def syncified(*args, **kwargs): - coro = method(*args, **kwargs) - loop = asyncio.get_event_loop() - if loop.is_running(): - return coro - else: - return loop.run_until_complete(coro) - - # Save an accessible reference to the original method - setattr(syncified, '__tl.sync', method) - setattr(t, method_name, syncified) - - -def syncify(*types): - """ - Converts all the methods in the given types (class definitions) - into synchronous, which return either the coroutine or the result - based on whether ``asyncio's`` event loop is running. - """ - # Our asynchronous generators all are `RequestIter`, which already - # provide a synchronous iterator variant, so we don't need to worry - # about asyncgenfunction's here. - for t in types: - for name in dir(t): - if not name.startswith('_') or name == '__call__': - if inspect.iscoroutinefunction(getattr(t, name)): - _syncify_wrap(t, name) - - -syncify(TelegramClient, _TakeoutClient, Draft, Dialog, MessageButton, - ChatGetter, SenderGetter, Forward, Message, InlineResult, Conversation) - - -# Private special case, since a conversation's methods return -# futures (but the public function themselves are synchronous). -_syncify_wrap(Conversation, '_get_result') - -__all__ = [ - 'TelegramClient', 'Button', - 'types', 'functions', 'custom', 'errors', - 'events', 'utils', 'connection' -] diff --git a/telethon/tl/custom/conversation.py b/telethon/tl/custom/conversation.py index 6cb973d4..b99831f3 100644 --- a/telethon/tl/custom/conversation.py +++ b/telethon/tl/custom/conversation.py @@ -524,6 +524,3 @@ class Conversation(ChatGetter): del self._client._conversations[chat_id] self._cancel_all() - - __enter__ = helpers._sync_enter - __exit__ = helpers._sync_exit diff --git a/telethon_generator/generators/docs.py b/telethon_generator/generators/docs.py index 34b599ff..8b46e4d1 100755 --- a/telethon_generator/generators/docs.py +++ b/telethon_generator/generators/docs.py @@ -396,7 +396,7 @@ def _write_html_pages(tlobjects, methods, layer, input_res): docs.write('
') docs.write('''
\
-from telethon.sync import TelegramClient
+from telethon import TelegramClient
 from telethon import functions, types
 
 with TelegramClient(name, api_id, api_hash) as client:
diff --git a/tests/telethon/test_helpers.py b/tests/telethon/test_helpers.py
index 689db8af..5ac4a78e 100644
--- a/tests/telethon/test_helpers.py
+++ b/tests/telethon/test_helpers.py
@@ -14,43 +14,6 @@ def test_strip_text():
     # I can't interpret the rest of the code well enough yet
 
 
-class TestSyncifyAsyncContext:
-    class NoopContextManager:
-        def __init__(self, loop):
-            self.count = 0
-            self.loop = loop
-
-        async def __aenter__(self):
-            self.count += 1
-            return self
-
-        async def __aexit__(self, exc_type, *args):
-            assert exc_type is None
-            self.count -= 1
-
-        __enter__ = helpers._sync_enter
-        __exit__ = helpers._sync_exit
-
-    def test_sync_acontext(self, event_loop):
-        contm = self.NoopContextManager(event_loop)
-        assert contm.count == 0
-
-        with contm:
-            assert contm.count == 1
-
-        assert contm.count == 0
-
-    @pytest.mark.asyncio
-    async def test_async_acontext(self, event_loop):
-        contm = self.NoopContextManager(event_loop)
-        assert contm.count == 0
-
-        async with contm:
-            assert contm.count == 1
-
-        assert contm.count == 0
-
-
 def test_generate_key_data_from_nonce():
     gkdfn = helpers.generate_key_data_from_nonce
 

From f86339ab1701c1043a971a08777da5cdbc3b47b8 Mon Sep 17 00:00:00 2001
From: Lonami Exo 
Date: Sat, 11 Sep 2021 14:16:25 +0200
Subject: [PATCH 037/256] Remove Conversation API

---
 readthedocs/misc/v2-migration-guide.rst       |  53 ++
 readthedocs/modules/custom.rst                |   9 -
 .../quick-references/client-reference.rst     |   1 -
 .../quick-references/objects-reference.rst    |  27 -
 telethon/_client/dialogs.py                   |  20 -
 telethon/_client/telegrambaseclient.py        |   3 -
 telethon/_client/telegramclient.py            | 131 +----
 telethon/_client/updates.py                   |  26 -
 telethon/errors/__init__.py                   |   2 +-
 telethon/errors/common.py                     |  11 -
 telethon/tl/custom/__init__.py                |   1 -
 telethon/tl/custom/conversation.py            | 526 ------------------
 12 files changed, 57 insertions(+), 753 deletions(-)
 delete mode 100644 telethon/tl/custom/conversation.py

diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst
index 5c748895..1ee9bda4 100644
--- a/readthedocs/misc/v2-migration-guide.rst
+++ b/readthedocs/misc/v2-migration-guide.rst
@@ -51,3 +51,56 @@ removed. This implies:
   * ``run_until_disconnected``
 
 // TODO provide standalone alternative for this?
+
+
+The Conversation API has been removed
+-------------------------------------
+
+This API had certain shortcomings, such as lacking persistence, poor interaction with other event
+handlers, and overcomplicated usage for anything beyond the simplest case.
+
+It is not difficult to write your own code to deal with a conversation's state. A simple
+`Finite State Machine `__ inside your handlers will do
+just fine:
+
+.. code-block:: python
+
+    from enum import Enum, auto
+
+    # We use a Python Enum for the state because it's a clean and easy way to do it
+    class State(Enum):
+        WAIT_NAME = auto()
+        WAIT_AGE = auto()
+
+    # The state in which different users are, {user_id: state}
+    conversation_state = {}
+
+    # ...code to create and setup your client...
+
+    @client.on(events.NewMessage)
+    async def handler(event):
+        who = event.sender_id
+        state = conversation_state.get(who)
+
+        if state is None:
+            # Starting a conversation
+            await event.respond('Hi! What is your name?')
+            conversation_state[who] = State.WAIT_NAME
+
+        elif state == State.WAIT_NAME:
+            name = event.text  # Save the name wherever you want
+            await event.respond('Nice! What is your age?')
+            conversation_state[who] = State.WAIT_AGE
+
+        elif state == State.WAIT_AGE:
+            age = event.text  # Save the age wherever you want
+            await event.respond('Thank you!')
+            # Conversation is done so we can forget the state of this user
+            del conversation_state[who]
+
+    # ...code to keep Telethon running...
+
+Not only is this approach simpler, but it can also be easily persisted, and you can adjust it
+to your needs and your handlers much more easily.
+
+// TODO provide standalone alternative for this?
diff --git a/readthedocs/modules/custom.rst b/readthedocs/modules/custom.rst
index 074b2161..01284fbb 100644
--- a/readthedocs/modules/custom.rst
+++ b/readthedocs/modules/custom.rst
@@ -46,15 +46,6 @@ ChatGetter
     :show-inheritance:
 
 
-Conversation
-============
-
-.. automodule:: telethon.tl.custom.conversation
-    :members:
-    :undoc-members:
-    :show-inheritance:
-
-
 Dialog
 ======
 
diff --git a/readthedocs/quick-references/client-reference.rst b/readthedocs/quick-references/client-reference.rst
index 6dd8245c..22517288 100644
--- a/readthedocs/quick-references/client-reference.rst
+++ b/readthedocs/quick-references/client-reference.rst
@@ -107,7 +107,6 @@ Dialogs
     iter_drafts
     get_drafts
     delete_dialog
-    conversation
 
 Users
 -----
diff --git a/readthedocs/quick-references/objects-reference.rst b/readthedocs/quick-references/objects-reference.rst
index 51ed4607..41f73033 100644
--- a/readthedocs/quick-references/objects-reference.rst
+++ b/readthedocs/quick-references/objects-reference.rst
@@ -155,33 +155,6 @@ its name, bot-API style file ID, etc.
     sticker_set
 
 
-Conversation
-============
-
-The `Conversation ` object
-is returned by the `client.conversation()
-` method to easily
-send and receive responses like a normal conversation.
-
-It bases `ChatGetter `.
-
-.. currentmodule:: telethon.tl.custom.conversation.Conversation
-
-.. autosummary::
-    :nosignatures:
-
-    send_message
-    send_file
-    mark_read
-    get_response
-    get_reply
-    get_edit
-    wait_read
-    wait_event
-    cancel
-    cancel_all
-
-
 AdminLogEvent
 =============
 
diff --git a/telethon/_client/dialogs.py b/telethon/_client/dialogs.py
index 67c47458..8471c7fb 100644
--- a/telethon/_client/dialogs.py
+++ b/telethon/_client/dialogs.py
@@ -252,23 +252,3 @@ async def delete_dialog(
         await self(functions.messages.DeleteHistoryRequest(entity, 0, revoke=revoke))
 
     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:
-    return custom.Conversation(
-        self,
-        entity,
-        timeout=timeout,
-        total_timeout=total_timeout,
-        max_messages=max_messages,
-        exclusive=exclusive,
-        replies_are_responses=replies_are_responses
-
-    )
diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py
index 16822d6a..256e8e6f 100644
--- a/telethon/_client/telegrambaseclient.py
+++ b/telethon/_client/telegrambaseclient.py
@@ -267,9 +267,6 @@ def init(
     # Some further state for subclasses
     self._event_builders = []
 
-    # {chat_id: {Conversation}}
-    self._conversations = collections.defaultdict(set)
-
     # Hack to workaround the fact Telegram may send album updates as
     # different Updates when being sent from a different data center.
     # {grouped_id: AlbumHack}
diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py
index 1c50a805..01cd5d14 100644
--- a/telethon/_client/telegramclient.py
+++ b/telethon/_client/telegramclient.py
@@ -151,9 +151,9 @@ class TelegramClient:
             will be received from Telegram as they occur.
 
             Turning this off means that Telegram will not send updates at all
-            so event handlers, conversations, and QR login will not work.
-            However, certain scripts don't need updates, so this will reduce
-            the amount of bandwidth used.
+            so event handlers and QR login will not work. However, certain
+            scripts don't need updates, so this will reduce the amount of
+            bandwidth used.
     """
 
     # region Account
@@ -1702,131 +1702,6 @@ class TelegramClient:
         """
         return dialogs.delete_dialog(**locals())
 
-    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 `
-        with the given entity.
-
-        .. 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
-                `
-                and a subsequent call to `conv.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 `.
-
-        Example
-            .. code-block:: python
-
-                #  denotes outgoing messages you sent
-                #  denotes incoming response messages
-                with bot.conversation(chat) as conv:
-                    #  Hi!
-                    conv.send_message('Hi!')
-
-                    #  Hello!
-                    hello = conv.get_response()
-
-                    #  Please tell me your name
-                    conv.send_message('Please tell me your name')
-
-                    #  ?
-                    name = conv.get_response().raw_text
-
-                    while not any(x.isalpha() for x in name):
-                        #  Your name didn't have any letters! Try again
-                        conv.send_message("Your name didn't have any letters! Try again")
-
-                        #  Human
-                        name = conv.get_response().raw_text
-
-                    #  Thanks Human!
-                    conv.send_message('Thanks {}!'.format(name))
-        """
-        return dialogs.conversation(**locals())
-
     # endregion Dialogs
 
     # region Downloads
diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py
index 04d7fbfb..c5d04ade 100644
--- a/telethon/_client/updates.py
+++ b/telethon/_client/updates.py
@@ -418,22 +418,6 @@ async def _dispatch_update(self: 'TelegramClient', update, others, channel_id, p
             pass  # might not have connection
 
     built = EventBuilderDict(self, update, others)
-    for conv_set in self._conversations.values():
-        for conv in conv_set:
-            ev = built[events.NewMessage]
-            if ev:
-                conv._on_new_message(ev)
-
-            ev = built[events.MessageEdited]
-            if ev:
-                conv._on_edit(ev)
-
-            ev = built[events.MessageRead]
-            if ev:
-                conv._on_read(ev)
-
-            if conv._custom:
-                await conv._check_custom(built)
 
     for builder, callback in self._event_builders:
         event = built[type(builder)]
@@ -451,11 +435,6 @@ async def _dispatch_update(self: 'TelegramClient', update, others, channel_id, p
 
         try:
             await callback(event)
-        except errors.AlreadyInConversationError:
-            name = getattr(callback, '__name__', repr(callback))
-            self._log[__name__].debug(
-                'Event handler "%s" already has an open conversation, '
-                'ignoring new one', name)
         except events.StopPropagation:
             name = getattr(callback, '__name__', repr(callback))
             self._log[__name__].debug(
@@ -492,11 +471,6 @@ async def _dispatch_event(self: 'TelegramClient', event):
 
         try:
             await callback(event)
-        except errors.AlreadyInConversationError:
-            name = getattr(callback, '__name__', repr(callback))
-            self._log[__name__].debug(
-                'Event handler "%s" already has an open conversation, '
-                'ignoring new one', name)
         except events.StopPropagation:
             name = getattr(callback, '__name__', repr(callback))
             self._log[__name__].debug(
diff --git a/telethon/errors/__init__.py b/telethon/errors/__init__.py
index f6bc16e5..a50ae36b 100644
--- a/telethon/errors/__init__.py
+++ b/telethon/errors/__init__.py
@@ -7,7 +7,7 @@ import re
 from .common import (
     ReadCancelledError, TypeNotFoundError, InvalidChecksumError,
     InvalidBufferError, SecurityError, CdnFileTamperedError,
-    AlreadyInConversationError, BadMessageError, MultiError
+    BadMessageError, MultiError
 )
 
 # This imports the base errors too, as they're imported there
diff --git a/telethon/errors/common.py b/telethon/errors/common.py
index de7d95f8..3ac246b3 100644
--- a/telethon/errors/common.py
+++ b/telethon/errors/common.py
@@ -79,17 +79,6 @@ class CdnFileTamperedError(SecurityError):
         )
 
 
-class AlreadyInConversationError(Exception):
-    """
-    Occurs when another exclusive conversation is opened in the same chat.
-    """
-    def __init__(self):
-        super().__init__(
-            'Cannot open exclusive conversation in a '
-            'chat that already has one open conversation'
-        )
-
-
 class BadMessageError(Exception):
     """Occurs when handling a bad_message_notification."""
     ErrorMessages = {
diff --git a/telethon/tl/custom/__init__.py b/telethon/tl/custom/__init__.py
index 9804969e..00a0d00f 100644
--- a/telethon/tl/custom/__init__.py
+++ b/telethon/tl/custom/__init__.py
@@ -9,6 +9,5 @@ from .button import Button
 from .inlinebuilder import InlineBuilder
 from .inlineresult import InlineResult
 from .inlineresults import InlineResults
-from .conversation import Conversation
 from .qrlogin import QRLogin
 from .participantpermissions import ParticipantPermissions
diff --git a/telethon/tl/custom/conversation.py b/telethon/tl/custom/conversation.py
deleted file mode 100644
index b99831f3..00000000
--- a/telethon/tl/custom/conversation.py
+++ /dev/null
@@ -1,526 +0,0 @@
-import asyncio
-import functools
-import inspect
-import itertools
-import time
-
-from .chatgetter import ChatGetter
-from ... import helpers, utils, errors
-
-# Sometimes the edits arrive very fast (within the same second).
-# In that case we add a small delta so that the age is older, for
-# comparision purposes. This value is enough for up to 1000 messages.
-_EDIT_COLLISION_DELTA = 0.001
-
-
-def _checks_cancelled(f):
-    @functools.wraps(f)
-    def wrapper(self, *args, **kwargs):
-        if self._cancelled:
-            raise asyncio.CancelledError('The conversation was cancelled before')
-
-        return f(self, *args, **kwargs)
-    return wrapper
-
-
-class Conversation(ChatGetter):
-    """
-    Represents a conversation inside an specific chat.
-
-    A conversation keeps track of new messages since it was
-    created until its exit and easily lets you query the
-    current state.
-
-    If you need a conversation across two or more chats,
-    you should use two conversations and synchronize them
-    as you better see fit.
-    """
-    _id_counter = 0
-    _custom_counter = 0
-
-    def __init__(self, client, input_chat,
-                 *, timeout, total_timeout, max_messages,
-                 exclusive, replies_are_responses):
-        # This call resets the client
-        ChatGetter.__init__(self, input_chat=input_chat)
-
-        self._id = Conversation._id_counter
-        Conversation._id_counter += 1
-
-        self._client = client
-        self._timeout = timeout
-        self._total_timeout = total_timeout
-        self._total_due = None
-
-        self._outgoing = set()
-        self._last_outgoing = 0
-        self._incoming = []
-        self._last_incoming = 0
-        self._max_incoming = max_messages
-        self._last_read = None
-        self._custom = {}
-
-        self._pending_responses = {}
-        self._pending_replies = {}
-        self._pending_edits = {}
-        self._pending_reads = {}
-
-        self._exclusive = exclusive
-        self._cancelled = False
-
-        # The user is able to expect two responses for the same message.
-        # {desired message ID: next incoming index}
-        self._response_indices = {}
-        if replies_are_responses:
-            self._reply_indices = self._response_indices
-        else:
-            self._reply_indices = {}
-
-        self._edit_dates = {}
-
-    @_checks_cancelled
-    async def send_message(self, *args, **kwargs):
-        """
-        Sends a message in the context of this conversation. Shorthand
-        for `telethon.client.messages.MessageMethods.send_message` with
-        ``entity`` already set.
-        """
-        sent = await self._client.send_message(
-            self._input_chat, *args, **kwargs)
-
-        # Albums will be lists, so handle that
-        ms = sent if isinstance(sent, list) else (sent,)
-        self._outgoing.update(m.id for m in ms)
-        self._last_outgoing = ms[-1].id
-        return sent
-
-    @_checks_cancelled
-    async def send_file(self, *args, **kwargs):
-        """
-        Sends a file in the context of this conversation. Shorthand
-        for `telethon.client.uploads.UploadMethods.send_file` with
-        ``entity`` already set.
-        """
-        sent = await self._client.send_file(
-            self._input_chat, *args, **kwargs)
-
-        # Albums will be lists, so handle that
-        ms = sent if isinstance(sent, list) else (sent,)
-        self._outgoing.update(m.id for m in ms)
-        self._last_outgoing = ms[-1].id
-        return sent
-
-    @_checks_cancelled
-    def mark_read(self, message=None):
-        """
-        Marks as read the latest received message if ``message is None``.
-        Otherwise, marks as read until the given message (or message ID).
-
-        This is equivalent to calling `client.send_read_acknowledge
-        `.
-        """
-        if message is None:
-            if self._incoming:
-                message = self._incoming[-1].id
-            else:
-                message = 0
-        elif not isinstance(message, int):
-            message = message.id
-
-        return self._client.send_read_acknowledge(
-            self._input_chat, max_id=message)
-
-    def get_response(self, message=None, *, timeout=None):
-        """
-        Gets the next message that responds to a previous one. This is
-        the method you need most of the time, along with `get_edit`.
-
-        Args:
-            message (`Message ` | `int`, optional):
-                The message (or the message ID) for which a response
-                is expected. By default this is the last sent message.
-
-            timeout (`int` | `float`, optional):
-                If present, this `timeout` (in seconds) will override the
-                per-action timeout defined for the conversation.
-
-        .. code-block:: python
-
-            async with client.conversation(...) as conv:
-                await conv.send_message('Hey, what is your name?')
-
-                response = await conv.get_response()
-                name = response.text
-
-                await conv.send_message('Nice to meet you, {}!'.format(name))
-        """
-        return self._get_message(
-            message, self._response_indices, self._pending_responses, timeout,
-            lambda x, y: True
-        )
-
-    def get_reply(self, message=None, *, timeout=None):
-        """
-        Gets the next message that explicitly replies to a previous one.
-        """
-        return self._get_message(
-            message, self._reply_indices, self._pending_replies, timeout,
-            lambda x, y: x.reply_to and x.reply_to.reply_to_msg_id == y
-        )
-
-    def _get_message(
-            self, target_message, indices, pending, timeout, condition):
-        """
-        Gets the next desired message under the desired condition.
-
-        Args:
-            target_message (`object`):
-                The target message for which we want to find another
-                response that applies based on `condition`.
-
-            indices (`dict`):
-                This dictionary remembers the last ID chosen for the
-                input `target_message`.
-
-            pending (`dict`):
-                This dictionary remembers {msg_id: Future} to be set
-                once `condition` is met.
-
-            timeout (`int`):
-                The timeout (in seconds) override to use for this operation.
-
-            condition (`callable`):
-                The condition callable that checks if an incoming
-                message is a valid response.
-        """
-        start_time = time.time()
-        target_id = self._get_message_id(target_message)
-
-        # If there is no last-chosen ID, make sure to pick one *after*
-        # the input message, since we don't want responses back in time
-        if target_id not in indices:
-            for i, incoming in enumerate(self._incoming):
-                if incoming.id > target_id:
-                    indices[target_id] = i
-                    break
-            else:
-                indices[target_id] = len(self._incoming)
-
-        # We will always return a future from here, even if the result
-        # can be set immediately. Otherwise, needing to await only
-        # sometimes is an annoying edge case (i.e. we would return
-        # a `Message` but `get_response()` always `await`'s).
-        future = self._client.loop.create_future()
-
-        # If there are enough responses saved return the next one
-        last_idx = indices[target_id]
-        if last_idx < len(self._incoming):
-            incoming = self._incoming[last_idx]
-            if condition(incoming, target_id):
-                indices[target_id] += 1
-                future.set_result(incoming)
-                return future
-
-        # Otherwise the next incoming response will be the one to use
-        #
-        # Note how we fill "pending" before giving control back to the
-        # event loop through "await". We want to register it as soon as
-        # possible, since any other task switch may arrive with the result.
-        pending[target_id] = future
-        return self._get_result(future, start_time, timeout, pending, target_id)
-
-    def get_edit(self, message=None, *, timeout=None):
-        """
-        Awaits for an edit after the last message to arrive.
-        The arguments are the same as those for `get_response`.
-        """
-        start_time = time.time()
-        target_id = self._get_message_id(message)
-
-        target_date = self._edit_dates.get(target_id, 0)
-        earliest_edit = min(
-            (x for x in self._incoming
-             if x.edit_date
-             and x.id > target_id
-             and x.edit_date.timestamp() > target_date
-             ),
-            key=lambda x: x.edit_date.timestamp(),
-            default=None
-        )
-
-        future = self._client.loop.create_future()
-        if earliest_edit and earliest_edit.edit_date.timestamp() > target_date:
-            self._edit_dates[target_id] = earliest_edit.edit_date.timestamp()
-            future.set_result(earliest_edit)
-            return future  # we should always return something we can await
-
-        # Otherwise the next incoming response will be the one to use
-        self._pending_edits[target_id] = future
-        return self._get_result(future, start_time, timeout, self._pending_edits, target_id)
-
-    def wait_read(self, message=None, *, timeout=None):
-        """
-        Awaits for the sent message to be marked as read. Note that
-        receiving a response doesn't imply the message was read, and
-        this action will also trigger even without a response.
-        """
-        start_time = time.time()
-        future = self._client.loop.create_future()
-        target_id = self._get_message_id(message)
-
-        if self._last_read is None:
-            self._last_read = target_id - 1
-
-        if self._last_read >= target_id:
-            return
-
-        self._pending_reads[target_id] = future
-        return self._get_result(future, start_time, timeout, self._pending_reads, target_id)
-
-    async def wait_event(self, event, *, timeout=None):
-        """
-        Waits for a custom event to occur. Timeouts still apply.
-
-        .. note::
-
-            **Only use this if there isn't another method available!**
-            For example, don't use `wait_event` for new messages,
-            since `get_response` already exists, etc.
-
-        Unless you're certain that your code will run fast enough,
-        generally you should get a "handle" of this special coroutine
-        before acting. In this example you will see how to wait for a user
-        to join a group with proper use of `wait_event`:
-
-        .. code-block:: python
-
-            from telethon import TelegramClient, events
-
-            client = TelegramClient(...)
-            group_id = ...
-
-            async def main():
-                # Could also get the user id from an event; this is just an example
-                user_id = ...
-
-                async with client.conversation(user_id) as conv:
-                    # Get a handle to the future event we'll wait for
-                    handle = conv.wait_event(events.ChatAction(
-                        group_id,
-                        func=lambda e: e.user_joined and e.user_id == user_id
-                    ))
-
-                    # Perform whatever action in between
-                    await conv.send_message('Please join this group before speaking to me!')
-
-                    # Wait for the event we registered above to fire
-                    event = await handle
-
-                    # Continue with the conversation
-                    await conv.send_message('Thanks!')
-
-        This way your event can be registered before acting,
-        since the response may arrive before your event was
-        registered. It depends on your use case since this
-        also means the event can arrive before you send
-        a previous action.
-        """
-        start_time = time.time()
-        if isinstance(event, type):
-            event = event()
-
-        await event.resolve(self._client)
-
-        counter = Conversation._custom_counter
-        Conversation._custom_counter += 1
-
-        future = self._client.loop.create_future()
-        self._custom[counter] = (event, future)
-        try:
-            return await self._get_result(future, start_time, timeout, self._custom, counter)
-        finally:
-            # Need to remove it from the dict if it times out, else we may
-            # try and fail to set the result later (#1618).
-            self._custom.pop(counter, None)
-
-    async def _check_custom(self, built):
-        for key, (ev, fut) in list(self._custom.items()):
-            ev_type = type(ev)
-            inst = built[ev_type]
-
-            if inst:
-                filter = ev.filter(inst)
-                if inspect.isawaitable(filter):
-                    filter = await filter
-
-                if filter:
-                    fut.set_result(inst)
-                    del self._custom[key]
-
-    def _on_new_message(self, response):
-        response = response.message
-        if response.chat_id != self.chat_id or response.out:
-            return
-
-        if len(self._incoming) == self._max_incoming:
-            self._cancel_all(ValueError('Too many incoming messages'))
-            return
-
-        self._incoming.append(response)
-
-        # Most of the time, these dictionaries will contain just one item
-        # TODO In fact, why not make it be that way? Force one item only.
-        #      How often will people want to wait for two responses at
-        #      the same time? It's impossible, first one will arrive
-        #      and then another, so they can do that.
-        for msg_id, future in list(self._pending_responses.items()):
-            self._response_indices[msg_id] = len(self._incoming)
-            future.set_result(response)
-            del self._pending_responses[msg_id]
-
-        for msg_id, future in list(self._pending_replies.items()):
-            if response.reply_to and msg_id == response.reply_to.reply_to_msg_id:
-                self._reply_indices[msg_id] = len(self._incoming)
-                future.set_result(response)
-                del self._pending_replies[msg_id]
-
-    def _on_edit(self, message):
-        message = message.message
-        if message.chat_id != self.chat_id or message.out:
-            return
-
-        # We have to update our incoming messages with the new edit date
-        for i, m in enumerate(self._incoming):
-            if m.id == message.id:
-                self._incoming[i] = message
-                break
-
-        for msg_id, future in list(self._pending_edits.items()):
-            if msg_id < message.id:
-                edit_ts = message.edit_date.timestamp()
-
-                # We compare <= because edit_ts resolution is always to
-                # seconds, but we may have increased _edit_dates before.
-                # Since the dates are ever growing this is not a problem.
-                if edit_ts <= self._edit_dates.get(msg_id, 0):
-                    self._edit_dates[msg_id] += _EDIT_COLLISION_DELTA
-                else:
-                    self._edit_dates[msg_id] = message.edit_date.timestamp()
-
-                future.set_result(message)
-                del self._pending_edits[msg_id]
-
-    def _on_read(self, event):
-        if event.chat_id != self.chat_id or event.inbox:
-            return
-
-        self._last_read = event.max_id
-
-        for msg_id, pending in list(self._pending_reads.items()):
-            if msg_id >= self._last_read:
-                pending.set_result(True)
-                del self._pending_reads[msg_id]
-
-    def _get_message_id(self, message):
-        if message is not None:  # 0 is valid but false-y, check for None
-            return message if isinstance(message, int) else message.id
-        elif self._last_outgoing:
-            return self._last_outgoing
-        else:
-            raise ValueError('No message was sent previously')
-
-    @_checks_cancelled
-    def _get_result(self, future, start_time, timeout, pending, target_id):
-        due = self._total_due
-        if timeout is None:
-            timeout = self._timeout
-
-        if timeout is not None:
-            due = min(due, start_time + timeout)
-
-        # NOTE: We can't try/finally to pop from pending here because
-        #       the event loop needs to get back to us, but it might
-        #       dispatch another update before, and in that case a
-        #       response could be set twice. So responses must be
-        #       cleared when their futures are set to a result.
-        return asyncio.wait_for(
-            future,
-            timeout=None if due == float('inf') else due - time.time()
-        )
-
-    def _cancel_all(self, exception=None):
-        self._cancelled = True
-        for pending in itertools.chain(
-                self._pending_responses.values(),
-                self._pending_replies.values(),
-                self._pending_edits.values()):
-            if exception:
-                pending.set_exception(exception)
-            else:
-                pending.cancel()
-
-        for _, fut in self._custom.values():
-            if exception:
-                fut.set_exception(exception)
-            else:
-                fut.cancel()
-
-    async def __aenter__(self):
-        self._input_chat = \
-            await self._client.get_input_entity(self._input_chat)
-
-        self._chat_peer = utils.get_peer(self._input_chat)
-
-        # Make sure we're the only conversation in this chat if it's exclusive
-        chat_id = utils.get_peer_id(self._chat_peer)
-        conv_set = self._client._conversations[chat_id]
-        if self._exclusive and conv_set:
-            raise errors.AlreadyInConversationError()
-
-        conv_set.add(self)
-        self._cancelled = False
-
-        self._last_outgoing = 0
-        self._last_incoming = 0
-        for d in (
-                self._outgoing, self._incoming,
-                self._pending_responses, self._pending_replies,
-                self._pending_edits, self._response_indices,
-                self._reply_indices, self._edit_dates, self._custom):
-            d.clear()
-
-        if self._total_timeout:
-            self._total_due = time.time() + self._total_timeout
-        else:
-            self._total_due = float('inf')
-
-        return self
-
-    def cancel(self):
-        """
-        Cancels the current conversation. Pending responses and subsequent
-        calls to get a response will raise ``asyncio.CancelledError``.
-
-        This method is synchronous and should not be awaited.
-        """
-        self._cancel_all()
-
-    async def cancel_all(self):
-        """
-        Calls `cancel` on *all* conversations in this chat.
-
-        Note that you should ``await`` this method, since it's meant to be
-        used outside of a context manager, and it needs to resolve the chat.
-        """
-        chat_id = await self._client.get_peer_id(self._input_chat)
-        for conv in self._client._conversations[chat_id]:
-            conv.cancel()
-
-    async def __aexit__(self, exc_type, exc_val, exc_tb):
-        chat_id = utils.get_peer_id(self._chat_peer)
-        conv_set = self._client._conversations[chat_id]
-        conv_set.discard(self)
-        if not conv_set:
-            del self._client._conversations[chat_id]
-
-        self._cancel_all()

From 66ef553adca172c4e49052b7cba1330082bf0af9 Mon Sep 17 00:00:00 2001
From: Lonami Exo 
Date: Sat, 11 Sep 2021 15:28:24 +0200
Subject: [PATCH 038/256] Remove duplicated docstrings

---
 telethon/_client/downloads.py    | 239 -------------------------------
 telethon/_client/messageparse.py |  32 -----
 telethon/_client/updates.py      | 142 ------------------
 telethon/_client/users.py        | 170 ----------------------
 4 files changed, 583 deletions(-)

diff --git a/telethon/_client/downloads.py b/telethon/_client/downloads.py
index 4500df33..aa8ed59a 100644
--- a/telethon/_client/downloads.py
+++ b/telethon/_client/downloads.py
@@ -195,43 +195,6 @@ async def download_profile_photo(
         file: 'hints.FileLike' = None,
         *,
         download_big: bool = True) -> typing.Optional[str]:
-    """
-    Downloads the profile photo from the given user, chat or channel.
-
-    Arguments
-        entity (`entity`):
-            From who the photo will be downloaded.
-
-            .. note::
-
-                This method expects the full entity (which has the data
-                to download the photo), not an input variant.
-
-                It's possible that sometimes you can't fetch the entity
-                from its input (since you can get errors like
-                ``ChannelPrivateError``) but you already have it through
-                another call, like getting a forwarded message from it.
-
-        file (`str` | `file`, optional):
-            The output file path, directory, or stream-like object.
-            If the path exists and is a file, it will be overwritten.
-            If file is the type `bytes`, it will be downloaded in-memory
-            as a bytestring (e.g. ``file=bytes``).
-
-        download_big (`bool`, optional):
-            Whether to use the big version of the available photos.
-
-    Returns
-        `None` if no photo was provided, or if it was Empty. On success
-        the file path is returned since it may differ from the one given.
-
-    Example
-        .. code-block:: python
-
-            # Download your own profile photo
-            path = await client.download_profile_photo('me')
-            print(path)
-    """
     # hex(crc32(x.encode('ascii'))) for x in
     # ('User', 'Chat', 'UserFull', 'ChatFull')
     ENTITIES = (0x2da17977, 0xc5af5d94, 0x1f4661b9, 0xd49a2697)
@@ -307,73 +270,6 @@ async def download_media(
         *,
         thumb: 'typing.Union[int, types.TypePhotoSize]' = None,
         progress_callback: 'hints.ProgressCallback' = None) -> typing.Optional[typing.Union[str, bytes]]:
-    """
-    Downloads the given media from a message object.
-
-    Note that if the download is too slow, you should consider installing
-    ``cryptg`` (through ``pip install cryptg``) so that decrypting the
-    received data is done in C instead of Python (much faster).
-
-    See also `Message.download_media() `.
-
-    Arguments
-        message (`Message ` | :tl:`Media`):
-            The media or message containing the media that will be downloaded.
-
-        file (`str` | `file`, optional):
-            The output file path, directory, or stream-like object.
-            If the path exists and is a file, it will be overwritten.
-            If file is the type `bytes`, it will be downloaded in-memory
-            as a bytestring (e.g. ``file=bytes``).
-
-        progress_callback (`callable`, optional):
-            A callback function accepting two parameters:
-            ``(received bytes, total)``.
-
-        thumb (`int` | :tl:`PhotoSize`, optional):
-            Which thumbnail size from the document or photo to download,
-            instead of downloading the document or photo itself.
-
-            If it's specified but the file does not have a thumbnail,
-            this method will return `None`.
-
-            The parameter should be an integer index between ``0`` and
-            ``len(sizes)``. ``0`` will download the smallest thumbnail,
-            and ``len(sizes) - 1`` will download the largest thumbnail.
-            You can also use negative indices, which work the same as
-            they do in Python's `list`.
-
-            You can also pass the :tl:`PhotoSize` instance to use.
-            Alternatively, the thumb size type `str` may be used.
-
-            In short, use ``thumb=0`` if you want the smallest thumbnail
-            and ``thumb=-1`` if you want the largest thumbnail.
-
-            .. note::
-                The largest thumbnail may be a video instead of a photo,
-                as they are available since layer 116 and are bigger than
-                any of the photos.
-
-    Returns
-        `None` if no media was provided, or if it was Empty. On success
-        the file path is returned since it may differ from the one given.
-
-    Example
-        .. code-block:: python
-
-            path = await client.download_media(message)
-            await client.download_media(message, filename)
-            # or
-            path = await message.download_media()
-            await message.download_media(filename)
-
-            # Printing download progress
-            def callback(current, total):
-                print('Downloaded', current, 'out of', total,
-                        'bytes: {:.2%}'.format(current / total))
-
-            await client.download_media(message, progress_callback=callback)
-    """
     # Downloading large documents may be slow enough to require a new file reference
     # to be obtained mid-download. Store (input chat, message id) so that the message
     # can be re-fetched.
@@ -428,58 +324,6 @@ async def download_file(
         dc_id: int = None,
         key: bytes = None,
         iv: bytes = None) -> typing.Optional[bytes]:
-    """
-    Low-level method to download files from their input location.
-
-    .. note::
-
-        Generally, you should instead use `download_media`.
-        This method is intended to be a bit more low-level.
-
-    Arguments
-        input_location (:tl:`InputFileLocation`):
-            The file location from which the file will be downloaded.
-            See `telethon.utils.get_input_location` source for a complete
-            list of supported types.
-
-        file (`str` | `file`, optional):
-            The output file path, directory, or stream-like object.
-            If the path exists and is a file, it will be overwritten.
-
-            If the file path is `None` or `bytes`, then the result
-            will be saved in memory and returned as `bytes`.
-
-        part_size_kb (`int`, optional):
-            Chunk size when downloading files. The larger, the less
-            requests will be made (up to 512KB maximum).
-
-        file_size (`int`, optional):
-            The file size that is about to be downloaded, if known.
-            Only used if ``progress_callback`` is specified.
-
-        progress_callback (`callable`, optional):
-            A callback function accepting two parameters:
-            ``(downloaded bytes, total)``. Note that the
-            ``total`` is the provided ``file_size``.
-
-        dc_id (`int`, optional):
-            The data center the library should connect to in order
-            to download the file. You shouldn't worry about this.
-
-        key ('bytes', optional):
-            In case of an encrypted upload (secret chats) a key is supplied
-
-        iv ('bytes', optional):
-            In case of an encrypted upload (secret chats) an iv is supplied
-
-
-    Example
-        .. code-block:: python
-
-            # Download a file and print its header
-            data = await client.download_file(input_file, bytes)
-            print(data[:16])
-    """
     return await self._download_file(
         input_location,
         file,
@@ -563,89 +407,6 @@ def iter_download(
         file_size: int = None,
         dc_id: int = None
 ):
-    """
-    Iterates over a file download, yielding chunks of the file.
-
-    This method can be used to stream files in a more convenient
-    way, since it offers more control (pausing, resuming, etc.)
-
-    .. note::
-
-        Using a value for `offset` or `stride` which is not a multiple
-        of the minimum allowed `request_size`, or if `chunk_size` is
-        different from `request_size`, the library will need to do a
-        bit more work to fetch the data in the way you intend it to.
-
-        You normally shouldn't worry about this.
-
-    Arguments
-        file (`hints.FileLike`):
-            The file of which contents you want to iterate over.
-
-        offset (`int`, optional):
-            The offset in bytes into the file from where the
-            download should start. For example, if a file is
-            1024KB long and you just want the last 512KB, you
-            would use ``offset=512 * 1024``.
-
-        stride (`int`, optional):
-            The stride of each chunk (how much the offset should
-            advance between reading each chunk). This parameter
-            should only be used for more advanced use cases.
-
-            It must be bigger than or equal to the `chunk_size`.
-
-        limit (`int`, optional):
-            The limit for how many *chunks* will be yielded at most.
-
-        chunk_size (`int`, optional):
-            The maximum size of the chunks that will be yielded.
-            Note that the last chunk may be less than this value.
-            By default, it equals to `request_size`.
-
-        request_size (`int`, optional):
-            How many bytes will be requested to Telegram when more
-            data is required. By default, as many bytes as possible
-            are requested. If you would like to request data in
-            smaller sizes, adjust this parameter.
-
-            Note that values outside the valid range will be clamped,
-            and the final value will also be a multiple of the minimum
-            allowed size.
-
-        file_size (`int`, optional):
-            If the file size is known beforehand, you should set
-            this parameter to said value. Depending on the type of
-            the input file passed, this may be set automatically.
-
-        dc_id (`int`, optional):
-            The data center the library should connect to in order
-            to download the file. You shouldn't worry about this.
-
-    Yields
-
-        `bytes` objects representing the chunks of the file if the
-        right conditions are met, or `memoryview` objects instead.
-
-    Example
-        .. code-block:: python
-
-            # Streaming `media` to an output file
-            # After the iteration ends, the sender is cleaned up
-            with open('photo.jpg', 'wb') as fd:
-                async for chunk in client.iter_download(media):
-                    fd.write(chunk)
-
-            # Fetching only the header of a file (32 bytes)
-            # You should manually close the iterator in this case.
-            #
-            # "stream" is a common name for asynchronous generators,
-            # and iter_download will yield `bytes` (chunks of the file).
-            stream = client.iter_download(media, request_size=32)
-            header = await stream.__anext__()  # "manual" version of `async for`
-            await stream.close()
-            assert len(header) == 32
-    """
     return self._iter_download(
         file,
         offset=offset,
diff --git a/telethon/_client/messageparse.py b/telethon/_client/messageparse.py
index 72b121a9..0cbdb40c 100644
--- a/telethon/_client/messageparse.py
+++ b/telethon/_client/messageparse.py
@@ -10,38 +10,6 @@ if typing.TYPE_CHECKING:
 
 
 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.
-
-    When setting a different value it should be one of:
-
-    * 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 ``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])``.
-
-    The ``unparse`` method should be the inverse of ``parse`` such
-    that ``assert text == unparse(*parse(text))``.
-
-    See :tl:`MessageEntity` for allowed message entities.
-
-    Example
-        .. code-block:: python
-
-            # Disabling default formatting
-            client.parse_mode = None
-
-            # Enabling HTML as the default format
-            client.parse_mode = 'html'
-    """
     return self._parse_mode
 
 def set_parse_mode(self: 'TelegramClient', mode: str):
diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py
index c5d04ade..89816530 100644
--- a/telethon/_client/updates.py
+++ b/telethon/_client/updates.py
@@ -30,74 +30,14 @@ async def _run_until_disconnected(self: 'TelegramClient'):
         await self.disconnect()
 
 async def set_receive_updates(self: 'TelegramClient', receive_updates):
-    """
-    Change the value of `receive_updates`.
-
-    This is an `async` method, because in order for Telegram to start
-    sending updates again, a request must be made.
-    """
     self._no_updates = not receive_updates
     if receive_updates:
         await self(functions.updates.GetStateRequest())
 
 async def run_until_disconnected(self: 'TelegramClient'):
-    """
-    Runs the event loop until the library is disconnected.
-
-    It also notifies Telegram that we want to receive updates
-    as described in https://core.telegram.org/api/updates.
-
-    Manual disconnections can be made by calling `disconnect()
-    `
-    or sending a ``KeyboardInterrupt`` (e.g. by pressing ``Ctrl+C`` on
-    the console window running the script).
-
-    If a disconnection error occurs (i.e. the library fails to reconnect
-    automatically), said error will be raised through here, so you have a
-    chance to ``except`` it on your own code.
-
-    If the loop is already running, this method returns a coroutine
-    that you should await on your own code.
-
-    .. note::
-
-        If you want to handle ``KeyboardInterrupt`` in your code,
-        simply run the event loop in your code too in any way, such as
-        ``loop.run_forever()`` or ``await client.disconnected`` (e.g.
-        ``loop.run_until_complete(client.disconnected)``).
-
-    Example
-        .. code-block:: python
-
-            # Blocks the current task here until a disconnection occurs.
-            #
-            # You will still receive updates, since this prevents the
-            # script from exiting.
-            await client.run_until_disconnected()
-    """
     return await self._run_until_disconnected()
 
 def on(self: 'TelegramClient', event: EventBuilder):
-    """
-    Decorator used to `add_event_handler` more conveniently.
-
-
-    Arguments
-        event (`_EventBuilder` | `type`):
-            The event builder class or instance to be used,
-            for instance ``events.NewMessage``.
-
-    Example
-        .. code-block:: python
-
-            from telethon import TelegramClient, events
-            client = TelegramClient(...)
-
-            # Here we use client.on
-            @client.on(events.NewMessage)
-            async def handler(event):
-                ...
-    """
     def decorator(f):
         self.add_event_handler(f, event)
         return f
@@ -108,38 +48,6 @@ def add_event_handler(
         self: 'TelegramClient',
         callback: Callback,
         event: EventBuilder = None):
-    """
-    Registers a new event handler callback.
-
-    The callback will be called when the specified event occurs.
-
-    Arguments
-        callback (`callable`):
-            The callable function accepting one parameter to be used.
-
-            Note that if you have used `telethon.events.register` in
-            the callback, ``event`` will be ignored, and instead the
-            events you previously registered will be used.
-
-        event (`_EventBuilder` | `type`, optional):
-            The event builder class or instance to be used,
-            for instance ``events.NewMessage``.
-
-            If left unspecified, `telethon.events.raw.Raw` (the
-            :tl:`Update` objects with no further processing) will
-            be passed instead.
-
-    Example
-        .. code-block:: python
-
-            from telethon import TelegramClient, events
-            client = TelegramClient(...)
-
-            async def handler(event):
-                ...
-
-            client.add_event_handler(handler, events.NewMessage)
-    """
     builders = events._get_handlers(callback)
     if builders is not None:
         for event in builders:
@@ -157,27 +65,6 @@ def remove_event_handler(
         self: 'TelegramClient',
         callback: Callback,
         event: EventBuilder = None) -> int:
-    """
-    Inverse operation of `add_event_handler()`.
-
-    If no event is given, all events for this callback are removed.
-    Returns how many callbacks were removed.
-
-    Example
-        .. code-block:: python
-
-            @client.on(events.Raw)
-            @client.on(events.NewMessage)
-            async def handler(event):
-                ...
-
-            # Removes only the "Raw" handling
-            # "handler" will still receive "events.NewMessage"
-            client.remove_event_handler(handler, events.Raw)
-
-            # "handler" will stop receiving anything
-            client.remove_event_handler(handler)
-    """
     found = 0
     if event and not isinstance(event, type):
         event = type(event)
@@ -194,38 +81,9 @@ def remove_event_handler(
 
 def list_event_handlers(self: 'TelegramClient')\
         -> 'typing.Sequence[typing.Tuple[Callback, EventBuilder]]':
-    """
-    Lists all registered event handlers.
-
-    Returns
-        A list of pairs consisting of ``(callback, event)``.
-
-    Example
-        .. code-block:: python
-
-            @client.on(events.NewMessage(pattern='hello'))
-            async def on_greeting(event):
-                '''Greets someone'''
-                await event.reply('Hi')
-
-            for callback, event in client.list_event_handlers():
-                print(id(callback), type(event))
-    """
     return [(callback, event) for event, callback in self._event_builders]
 
 async def catch_up(self: 'TelegramClient'):
-    """
-    "Catches up" on the missed updates while the client was offline.
-    You should call this method after registering the event handlers
-    so that the updates it loads can by processed by your script.
-
-    This can also be used to forcibly fetch new updates if there are any.
-
-    Example
-        .. code-block:: python
-
-            await client.catch_up()
-    """
     pts, date = self._state_cache[None]
     if not pts:
         return
diff --git a/telethon/_client/users.py b/telethon/_client/users.py
index e6964e55..0d871878 100644
--- a/telethon/_client/users.py
+++ b/telethon/_client/users.py
@@ -129,26 +129,6 @@ async def call(self: 'TelegramClient', sender, request, ordered=False, flood_sle
 
 async def get_me(self: 'TelegramClient', input_peer: bool = False) \
         -> 'typing.Union[types.User, types.InputPeerUser]':
-    """
-    Gets "me", the current :tl:`User` who is logged in.
-
-    If the user has not logged in yet, this method returns `None`.
-
-    Arguments
-        input_peer (`bool`, optional):
-            Whether to return the :tl:`InputPeerUser` version or the normal
-            :tl:`User`. This can be useful if you just need to know the ID
-            of yourself.
-
-    Returns
-        Your own :tl:`User`.
-
-    Example
-        .. code-block:: python
-
-            me = await client.get_me()
-            print(me.username)
-    """
     if input_peer and self._self_input_peer:
         return self._self_input_peer
 
@@ -176,34 +156,12 @@ def _self_id(self: 'TelegramClient') -> typing.Optional[int]:
     return self._self_input_peer.user_id if self._self_input_peer else None
 
 async def is_bot(self: 'TelegramClient') -> bool:
-    """
-    Return `True` if the signed-in user is a bot, `False` otherwise.
-
-    Example
-        .. code-block:: python
-
-            if await client.is_bot():
-                print('Beep')
-            else:
-                print('Hello')
-    """
     if self._bot is None:
         self._bot = (await self.get_me()).bot
 
     return self._bot
 
 async def is_user_authorized(self: 'TelegramClient') -> bool:
-    """
-    Returns `True` if the user is authorized (logged in).
-
-    Example
-        .. code-block:: python
-
-            if not await client.is_user_authorized():
-                await client.send_code_request(phone)
-                code = input('enter code: ')
-                await client.sign_in(phone, code)
-    """
     if self._authorized is None:
         try:
             # Any request that requires authorization will work
@@ -217,59 +175,6 @@ async def is_user_authorized(self: 'TelegramClient') -> bool:
 async def get_entity(
         self: 'TelegramClient',
         entity: 'hints.EntitiesLike') -> 'hints.Entity':
-    """
-    Turns the given entity into a valid Telegram :tl:`User`, :tl:`Chat`
-    or :tl:`Channel`. You can also pass a list or iterable of entities,
-    and they will be efficiently fetched from the network.
-
-    Arguments
-        entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`):
-            If a username is given, **the username will be resolved** making
-            an API call every time. Resolving usernames is an expensive
-            operation and will start hitting flood waits around 50 usernames
-            in a short period of time.
-
-            If you want to get the entity for a *cached* username, you should
-            first `get_input_entity(username) ` which will
-            use the cache), and then use `get_entity` with the result of the
-            previous call.
-
-            Similar limits apply to invite links, and you should use their
-            ID instead.
-
-            Using phone numbers (from people in your contact list), exact
-            names, integer IDs or :tl:`Peer` rely on a `get_input_entity`
-            first, which in turn needs the entity to be in cache, unless
-            a :tl:`InputPeer` was passed.
-
-            Unsupported types will raise ``TypeError``.
-
-            If the entity can't be found, ``ValueError`` will be raised.
-
-    Returns
-        :tl:`User`, :tl:`Chat` or :tl:`Channel` corresponding to the
-        input entity. A list will be returned if more than one was given.
-
-    Example
-        .. code-block:: python
-
-            from telethon import utils
-
-            me = await client.get_entity('me')
-            print(utils.get_display_name(me))
-
-            chat = await client.get_input_entity('username')
-            async for message in client.iter_messages(chat):
-                ...
-
-            # Note that you could have used the username directly, but it's
-            # good to use get_input_entity if you will reuse it a lot.
-            async for message in client.iter_messages('username'):
-                ...
-
-            # Note that for this to work the phone number must be in your contacts
-            some_id = await client.get_peer_id('+34123456789')
-    """
     single = not utils.is_list_like(entity)
     if single:
         entity = (entity,)
@@ -340,67 +245,6 @@ async def get_entity(
 async def get_input_entity(
         self: 'TelegramClient',
         peer: 'hints.EntityLike') -> 'types.TypeInputPeer':
-    """
-    Turns the given entity into its input entity version.
-
-    Most requests use this kind of :tl:`InputPeer`, so this is the most
-    suitable call to make for those cases. **Generally you should let the
-    library do its job** and don't worry about getting the input entity
-    first, but if you're going to use an entity often, consider making the
-    call:
-
-    Arguments
-        entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`):
-            If a username or invite link is given, **the library will
-            use the cache**. This means that it's possible to be using
-            a username that *changed* or an old invite link (this only
-            happens if an invite link for a small group chat is used
-            after it was upgraded to a mega-group).
-
-            If the username or ID from the invite link is not found in
-            the cache, it will be fetched. The same rules apply to phone
-            numbers (``'+34 123456789'``) from people in your contact list.
-
-            If an exact name is given, it must be in the cache too. This
-            is not reliable as different people can share the same name
-            and which entity is returned is arbitrary, and should be used
-            only for quick tests.
-
-            If a positive integer ID is given, the entity will be searched
-            in cached users, chats or channels, without making any call.
-
-            If a negative integer ID is given, the entity will be searched
-            exactly as either a chat (prefixed with ``-``) or as a channel
-            (prefixed with ``-100``).
-
-            If a :tl:`Peer` is given, it will be searched exactly in the
-            cache as either a user, chat or channel.
-
-            If the given object can be turned into an input entity directly,
-            said operation will be done.
-
-            Unsupported types will raise ``TypeError``.
-
-            If the entity can't be found, ``ValueError`` will be raised.
-
-    Returns
-        :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.
-
-    Example
-        .. code-block:: python
-
-            # If you're going to use "username" often in your code
-            # (make a lot of calls), consider getting its input entity
-            # once, and then using the "user" everywhere instead.
-            user = await client.get_input_entity('username')
-
-            # The same applies to IDs, chats or channels.
-            chat = await client.get_input_entity(-123456789)
-    """
     # Short-circuit if the input parameter directly maps to an InputPeer
     try:
         return utils.get_input_peer(peer)
@@ -472,20 +316,6 @@ async def get_peer_id(
         self: 'TelegramClient',
         peer: 'hints.EntityLike',
         add_mark: bool = True) -> int:
-    """
-    Gets the ID for the given entity.
-
-    This method needs to be ``async`` because `peer` supports usernames,
-    invite-links, phone numbers (from people in your contact list), etc.
-
-    If ``add_mark is False``, then a positive ID will be returned
-    instead. By default, bot-API style IDs (signed) are returned.
-
-    Example
-        .. code-block:: python
-
-            print(await client.get_peer_id('me'))
-    """
     if isinstance(peer, int):
         return utils.get_peer_id(peer, add_mark=add_mark)
 

From a901d43a6d124af65fee622a51eef28d8f77743c Mon Sep 17 00:00:00 2001
From: Lonami Exo 
Date: Sat, 11 Sep 2021 17:48:23 +0200
Subject: [PATCH 039/256] Rename more subpackages and modules

---
 .gitignore                                    |   6 +-
 readthedocs/misc/v2-migration-guide.rst       | 111 +++++++++++-------
 telethon/{crypto => _crypto}/__init__.py      |   0
 telethon/{crypto => _crypto}/aes.py           |   0
 telethon/{crypto => _crypto}/aesctr.py        |   0
 telethon/{crypto => _crypto}/authkey.py       |   0
 telethon/{crypto => _crypto}/cdndecrypter.py  |   0
 telethon/{crypto => _crypto}/factorization.py |   0
 telethon/{crypto => _crypto}/libssl.py        |   0
 telethon/{crypto => _crypto}/rsa.py           |   0
 telethon/{extensions => _misc}/__init__.py    |   0
 .../{extensions => _misc}/binaryreader.py     |   0
 telethon/{ => _misc}/entitycache.py           |   0
 telethon/{ => _misc}/helpers.py               |   0
 telethon/{ => _misc}/hints.py                 |   0
 telethon/{extensions => _misc}/html.py        |   0
 telethon/{extensions => _misc}/markdown.py    |   0
 .../{extensions => _misc}/messagepacker.py    |   0
 telethon/{ => _misc}/password.py              |   0
 telethon/{ => _misc}/requestiter.py           |   0
 telethon/{ => _misc}/statecache.py            |   0
 telethon/{ => _misc}/utils.py                 |   0
 telethon/{network => _network}/__init__.py    |   0
 .../{network => _network}/authenticator.py    |   0
 .../connection/__init__.py                    |   0
 .../connection/connection.py                  |   0
 .../{network => _network}/connection/http.py  |   0
 .../connection/tcpabridged.py                 |   0
 .../connection/tcpfull.py                     |   0
 .../connection/tcpintermediate.py             |   0
 .../connection/tcpmtproxy.py                  |   0
 .../connection/tcpobfuscated.py               |   0
 .../mtprotoplainsender.py                     |   0
 .../{network => _network}/mtprotosender.py    |   0
 .../{network => _network}/mtprotostate.py     |   0
 .../{network => _network}/requeststate.py     |   0
 telethon/{tl => _tl}/__init__.py              |   0
 telethon/{tl => _tl}/core/__init__.py         |   0
 telethon/{tl => _tl}/core/gzippacked.py       |   0
 telethon/{tl => _tl}/core/messagecontainer.py |   0
 telethon/{tl => _tl}/core/rpcresult.py        |   0
 telethon/{tl => _tl}/core/tlmessage.py        |   0
 telethon/{tl => _tl}/custom/__init__.py       |   0
 telethon/{tl => _tl}/custom/adminlogevent.py  |   0
 telethon/{tl => _tl}/custom/button.py         |   0
 telethon/{tl => _tl}/custom/chatgetter.py     |   0
 telethon/{tl => _tl}/custom/dialog.py         |   0
 telethon/{tl => _tl}/custom/draft.py          |   0
 telethon/{tl => _tl}/custom/file.py           |   0
 telethon/{tl => _tl}/custom/forward.py        |   0
 telethon/{tl => _tl}/custom/inlinebuilder.py  |   0
 telethon/{tl => _tl}/custom/inlineresult.py   |   0
 telethon/{tl => _tl}/custom/inlineresults.py  |   0
 telethon/{tl => _tl}/custom/inputsizedfile.py |   0
 telethon/{tl => _tl}/custom/message.py        |   0
 telethon/{tl => _tl}/custom/messagebutton.py  |   0
 .../custom/participantpermissions.py          |   0
 telethon/{tl => _tl}/custom/qrlogin.py        |   0
 telethon/{tl => _tl}/custom/sendergetter.py   |   0
 telethon/{tl => _tl}/patched/__init__.py      |   0
 telethon/{tl => _tl}/tlobject.py              |   0
 61 files changed, 69 insertions(+), 48 deletions(-)
 rename telethon/{crypto => _crypto}/__init__.py (100%)
 rename telethon/{crypto => _crypto}/aes.py (100%)
 rename telethon/{crypto => _crypto}/aesctr.py (100%)
 rename telethon/{crypto => _crypto}/authkey.py (100%)
 rename telethon/{crypto => _crypto}/cdndecrypter.py (100%)
 rename telethon/{crypto => _crypto}/factorization.py (100%)
 rename telethon/{crypto => _crypto}/libssl.py (100%)
 rename telethon/{crypto => _crypto}/rsa.py (100%)
 rename telethon/{extensions => _misc}/__init__.py (100%)
 rename telethon/{extensions => _misc}/binaryreader.py (100%)
 rename telethon/{ => _misc}/entitycache.py (100%)
 rename telethon/{ => _misc}/helpers.py (100%)
 rename telethon/{ => _misc}/hints.py (100%)
 rename telethon/{extensions => _misc}/html.py (100%)
 rename telethon/{extensions => _misc}/markdown.py (100%)
 rename telethon/{extensions => _misc}/messagepacker.py (100%)
 rename telethon/{ => _misc}/password.py (100%)
 rename telethon/{ => _misc}/requestiter.py (100%)
 rename telethon/{ => _misc}/statecache.py (100%)
 rename telethon/{ => _misc}/utils.py (100%)
 rename telethon/{network => _network}/__init__.py (100%)
 rename telethon/{network => _network}/authenticator.py (100%)
 rename telethon/{network => _network}/connection/__init__.py (100%)
 rename telethon/{network => _network}/connection/connection.py (100%)
 rename telethon/{network => _network}/connection/http.py (100%)
 rename telethon/{network => _network}/connection/tcpabridged.py (100%)
 rename telethon/{network => _network}/connection/tcpfull.py (100%)
 rename telethon/{network => _network}/connection/tcpintermediate.py (100%)
 rename telethon/{network => _network}/connection/tcpmtproxy.py (100%)
 rename telethon/{network => _network}/connection/tcpobfuscated.py (100%)
 rename telethon/{network => _network}/mtprotoplainsender.py (100%)
 rename telethon/{network => _network}/mtprotosender.py (100%)
 rename telethon/{network => _network}/mtprotostate.py (100%)
 rename telethon/{network => _network}/requeststate.py (100%)
 rename telethon/{tl => _tl}/__init__.py (100%)
 rename telethon/{tl => _tl}/core/__init__.py (100%)
 rename telethon/{tl => _tl}/core/gzippacked.py (100%)
 rename telethon/{tl => _tl}/core/messagecontainer.py (100%)
 rename telethon/{tl => _tl}/core/rpcresult.py (100%)
 rename telethon/{tl => _tl}/core/tlmessage.py (100%)
 rename telethon/{tl => _tl}/custom/__init__.py (100%)
 rename telethon/{tl => _tl}/custom/adminlogevent.py (100%)
 rename telethon/{tl => _tl}/custom/button.py (100%)
 rename telethon/{tl => _tl}/custom/chatgetter.py (100%)
 rename telethon/{tl => _tl}/custom/dialog.py (100%)
 rename telethon/{tl => _tl}/custom/draft.py (100%)
 rename telethon/{tl => _tl}/custom/file.py (100%)
 rename telethon/{tl => _tl}/custom/forward.py (100%)
 rename telethon/{tl => _tl}/custom/inlinebuilder.py (100%)
 rename telethon/{tl => _tl}/custom/inlineresult.py (100%)
 rename telethon/{tl => _tl}/custom/inlineresults.py (100%)
 rename telethon/{tl => _tl}/custom/inputsizedfile.py (100%)
 rename telethon/{tl => _tl}/custom/message.py (100%)
 rename telethon/{tl => _tl}/custom/messagebutton.py (100%)
 rename telethon/{tl => _tl}/custom/participantpermissions.py (100%)
 rename telethon/{tl => _tl}/custom/qrlogin.py (100%)
 rename telethon/{tl => _tl}/custom/sendergetter.py (100%)
 rename telethon/{tl => _tl}/patched/__init__.py (100%)
 rename telethon/{tl => _tl}/tlobject.py (100%)

diff --git a/.gitignore b/.gitignore
index 6f2cf6f7..e81bec11 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,7 @@
 # Generated code
-/telethon/tl/functions/
-/telethon/tl/types/
-/telethon/tl/alltlobjects.py
+/telethon/_tl/functions/
+/telethon/_tl/types/
+/telethon/_tl/alltlobjects.py
 /telethon/errors/rpcerrorlist.py
 
 # User session
diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst
index 1ee9bda4..de1af171 100644
--- a/readthedocs/misc/v2-migration-guide.rst
+++ b/readthedocs/misc/v2-migration-guide.rst
@@ -9,6 +9,8 @@ the technical debt that has grown on the project.
 This document documents all the things you should be aware of when migrating
 from Telethon version 1.x to 2.0 onwards.
 
+**Please read this document in full before upgrading your code to Telethon 2.0.**
+
 
 User, chat and channel identifiers are now 64-bit numbers
 ---------------------------------------------------------
@@ -22,17 +24,75 @@ 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
-----------------------------
+Many subpackages and 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``).
+The following subpackages are now considered private:
 
+* ``client`` is now ``_client``.
+* ``crypto`` is now ``_crypto``.
+* ``extensions`` is now ``_misc``.
+* ``tl`` is now ``_tl``.
+
+The following modules have been moved inside ``_misc``:
+
+* ``entitycache.py``
+* ``helpers.py``
+* ``hints.py``
+* ``password.py``
+* ``requestiter.py`
+* ``statecache.py``
+* ``utils.py``
+
+
+The TelegramClient is no longer made out of mixins
+--------------------------------------------------
+
+If you were relying on any of the individual mixins that made up the client, such as
+``UserMethods`` inside the ``telethon.client`` subpackage, those are now gone.
+There is a single ``TelegramClient`` class now, containing everything you need.
+
+
+Raw API methods have been renamed
+---------------------------------
+
+The subpackage holding the raw API methods has been renamed from ``tl`` to ``_tl`` in order to
+signal that these are prone to change across minor version bumps (the ``y`` in version ``x.y.z``).
+
+The ``Request`` suffix has been removed from the classes inside ``tl.functions``.
+
+The ``tl.types`` is now simply ``_tl``, and the ``tl.functions`` is now ``_tl.fn``.
+
+Some examples:
+
+.. code-block:: python
+
+    # Before
+    from telethon.tl import types, functions
+
+    await client(functions.messages.SendMessageRequest(...))
+    message: types.Message = ...
+
+    # After
+    from telethon import _tl
+    await client(_tl.fn.messages.SendMessage(...))
+    message: _tl.Message
+
+This serves multiple goals:
+
+* It removes redundant parts from the names. The "recommended" way of using the raw API is through
+  the subpackage namespace, which already contains a mention to "functions" in it. In addition,
+  some requests were awkward, such as ``SendCustomRequestRequest``.
+* It makes it easier to search for code that is using the raw API, so that you can quickly
+  identify which parts are making use of it.
+* The name is shorter, but remains recognizable.
+
+// TODO this definitely generated files mapping from the original name to this new one...
 
 Synchronous compatibility mode has been removed
 -----------------------------------------------
@@ -61,46 +121,7 @@ handlers, and overcomplicated usage for anything beyond the simplest case.
 
 It is not difficult to write your own code to deal with a conversation's state. A simple
 `Finite State Machine `__ inside your handlers will do
-just fine:
-
-.. code-block:: python
-
-    from enum import Enum, auto
-
-    # We use a Python Enum for the state because it's a clean and easy way to do it
-    class State(Enum):
-        WAIT_NAME = auto()
-        WAIT_AGE = auto()
-
-    # The state in which different users are, {user_id: state}
-    conversation_state = {}
-
-    # ...code to create and setup your client...
-
-    @client.on(events.NewMessage)
-    async def handler(event):
-        who = event.sender_id
-        state = conversation_state.get(who)
-
-        if state is None:
-            # Starting a conversation
-            await event.respond('Hi! What is your name?')
-            conversation_state[who] = State.WAIT_NAME
-
-        elif state == State.WAIT_NAME:
-            name = event.text  # Save the name wherever you want
-            await event.respond('Nice! What is your age?')
-            conversation_state[who] = State.WAIT_AGE
-
-        elif state == State.WAIT_AGE:
-            age = event.text  # Save the age wherever you want
-            await event.respond('Thank you!')
-            # Conversation is done so we can forget the state of this user
-            del conversation_state[who]
-
-    # ...code to keep Telethon running...
-
-Not only is this approach simpler, but it can also be easily persisted, and you can adjust it
-to your needs and your handlers much more easily.
+just fine This approach can also be easily persisted, and you can adjust it to your needs and
+your handlers much more easily.
 
 // TODO provide standalone alternative for this?
diff --git a/telethon/crypto/__init__.py b/telethon/_crypto/__init__.py
similarity index 100%
rename from telethon/crypto/__init__.py
rename to telethon/_crypto/__init__.py
diff --git a/telethon/crypto/aes.py b/telethon/_crypto/aes.py
similarity index 100%
rename from telethon/crypto/aes.py
rename to telethon/_crypto/aes.py
diff --git a/telethon/crypto/aesctr.py b/telethon/_crypto/aesctr.py
similarity index 100%
rename from telethon/crypto/aesctr.py
rename to telethon/_crypto/aesctr.py
diff --git a/telethon/crypto/authkey.py b/telethon/_crypto/authkey.py
similarity index 100%
rename from telethon/crypto/authkey.py
rename to telethon/_crypto/authkey.py
diff --git a/telethon/crypto/cdndecrypter.py b/telethon/_crypto/cdndecrypter.py
similarity index 100%
rename from telethon/crypto/cdndecrypter.py
rename to telethon/_crypto/cdndecrypter.py
diff --git a/telethon/crypto/factorization.py b/telethon/_crypto/factorization.py
similarity index 100%
rename from telethon/crypto/factorization.py
rename to telethon/_crypto/factorization.py
diff --git a/telethon/crypto/libssl.py b/telethon/_crypto/libssl.py
similarity index 100%
rename from telethon/crypto/libssl.py
rename to telethon/_crypto/libssl.py
diff --git a/telethon/crypto/rsa.py b/telethon/_crypto/rsa.py
similarity index 100%
rename from telethon/crypto/rsa.py
rename to telethon/_crypto/rsa.py
diff --git a/telethon/extensions/__init__.py b/telethon/_misc/__init__.py
similarity index 100%
rename from telethon/extensions/__init__.py
rename to telethon/_misc/__init__.py
diff --git a/telethon/extensions/binaryreader.py b/telethon/_misc/binaryreader.py
similarity index 100%
rename from telethon/extensions/binaryreader.py
rename to telethon/_misc/binaryreader.py
diff --git a/telethon/entitycache.py b/telethon/_misc/entitycache.py
similarity index 100%
rename from telethon/entitycache.py
rename to telethon/_misc/entitycache.py
diff --git a/telethon/helpers.py b/telethon/_misc/helpers.py
similarity index 100%
rename from telethon/helpers.py
rename to telethon/_misc/helpers.py
diff --git a/telethon/hints.py b/telethon/_misc/hints.py
similarity index 100%
rename from telethon/hints.py
rename to telethon/_misc/hints.py
diff --git a/telethon/extensions/html.py b/telethon/_misc/html.py
similarity index 100%
rename from telethon/extensions/html.py
rename to telethon/_misc/html.py
diff --git a/telethon/extensions/markdown.py b/telethon/_misc/markdown.py
similarity index 100%
rename from telethon/extensions/markdown.py
rename to telethon/_misc/markdown.py
diff --git a/telethon/extensions/messagepacker.py b/telethon/_misc/messagepacker.py
similarity index 100%
rename from telethon/extensions/messagepacker.py
rename to telethon/_misc/messagepacker.py
diff --git a/telethon/password.py b/telethon/_misc/password.py
similarity index 100%
rename from telethon/password.py
rename to telethon/_misc/password.py
diff --git a/telethon/requestiter.py b/telethon/_misc/requestiter.py
similarity index 100%
rename from telethon/requestiter.py
rename to telethon/_misc/requestiter.py
diff --git a/telethon/statecache.py b/telethon/_misc/statecache.py
similarity index 100%
rename from telethon/statecache.py
rename to telethon/_misc/statecache.py
diff --git a/telethon/utils.py b/telethon/_misc/utils.py
similarity index 100%
rename from telethon/utils.py
rename to telethon/_misc/utils.py
diff --git a/telethon/network/__init__.py b/telethon/_network/__init__.py
similarity index 100%
rename from telethon/network/__init__.py
rename to telethon/_network/__init__.py
diff --git a/telethon/network/authenticator.py b/telethon/_network/authenticator.py
similarity index 100%
rename from telethon/network/authenticator.py
rename to telethon/_network/authenticator.py
diff --git a/telethon/network/connection/__init__.py b/telethon/_network/connection/__init__.py
similarity index 100%
rename from telethon/network/connection/__init__.py
rename to telethon/_network/connection/__init__.py
diff --git a/telethon/network/connection/connection.py b/telethon/_network/connection/connection.py
similarity index 100%
rename from telethon/network/connection/connection.py
rename to telethon/_network/connection/connection.py
diff --git a/telethon/network/connection/http.py b/telethon/_network/connection/http.py
similarity index 100%
rename from telethon/network/connection/http.py
rename to telethon/_network/connection/http.py
diff --git a/telethon/network/connection/tcpabridged.py b/telethon/_network/connection/tcpabridged.py
similarity index 100%
rename from telethon/network/connection/tcpabridged.py
rename to telethon/_network/connection/tcpabridged.py
diff --git a/telethon/network/connection/tcpfull.py b/telethon/_network/connection/tcpfull.py
similarity index 100%
rename from telethon/network/connection/tcpfull.py
rename to telethon/_network/connection/tcpfull.py
diff --git a/telethon/network/connection/tcpintermediate.py b/telethon/_network/connection/tcpintermediate.py
similarity index 100%
rename from telethon/network/connection/tcpintermediate.py
rename to telethon/_network/connection/tcpintermediate.py
diff --git a/telethon/network/connection/tcpmtproxy.py b/telethon/_network/connection/tcpmtproxy.py
similarity index 100%
rename from telethon/network/connection/tcpmtproxy.py
rename to telethon/_network/connection/tcpmtproxy.py
diff --git a/telethon/network/connection/tcpobfuscated.py b/telethon/_network/connection/tcpobfuscated.py
similarity index 100%
rename from telethon/network/connection/tcpobfuscated.py
rename to telethon/_network/connection/tcpobfuscated.py
diff --git a/telethon/network/mtprotoplainsender.py b/telethon/_network/mtprotoplainsender.py
similarity index 100%
rename from telethon/network/mtprotoplainsender.py
rename to telethon/_network/mtprotoplainsender.py
diff --git a/telethon/network/mtprotosender.py b/telethon/_network/mtprotosender.py
similarity index 100%
rename from telethon/network/mtprotosender.py
rename to telethon/_network/mtprotosender.py
diff --git a/telethon/network/mtprotostate.py b/telethon/_network/mtprotostate.py
similarity index 100%
rename from telethon/network/mtprotostate.py
rename to telethon/_network/mtprotostate.py
diff --git a/telethon/network/requeststate.py b/telethon/_network/requeststate.py
similarity index 100%
rename from telethon/network/requeststate.py
rename to telethon/_network/requeststate.py
diff --git a/telethon/tl/__init__.py b/telethon/_tl/__init__.py
similarity index 100%
rename from telethon/tl/__init__.py
rename to telethon/_tl/__init__.py
diff --git a/telethon/tl/core/__init__.py b/telethon/_tl/core/__init__.py
similarity index 100%
rename from telethon/tl/core/__init__.py
rename to telethon/_tl/core/__init__.py
diff --git a/telethon/tl/core/gzippacked.py b/telethon/_tl/core/gzippacked.py
similarity index 100%
rename from telethon/tl/core/gzippacked.py
rename to telethon/_tl/core/gzippacked.py
diff --git a/telethon/tl/core/messagecontainer.py b/telethon/_tl/core/messagecontainer.py
similarity index 100%
rename from telethon/tl/core/messagecontainer.py
rename to telethon/_tl/core/messagecontainer.py
diff --git a/telethon/tl/core/rpcresult.py b/telethon/_tl/core/rpcresult.py
similarity index 100%
rename from telethon/tl/core/rpcresult.py
rename to telethon/_tl/core/rpcresult.py
diff --git a/telethon/tl/core/tlmessage.py b/telethon/_tl/core/tlmessage.py
similarity index 100%
rename from telethon/tl/core/tlmessage.py
rename to telethon/_tl/core/tlmessage.py
diff --git a/telethon/tl/custom/__init__.py b/telethon/_tl/custom/__init__.py
similarity index 100%
rename from telethon/tl/custom/__init__.py
rename to telethon/_tl/custom/__init__.py
diff --git a/telethon/tl/custom/adminlogevent.py b/telethon/_tl/custom/adminlogevent.py
similarity index 100%
rename from telethon/tl/custom/adminlogevent.py
rename to telethon/_tl/custom/adminlogevent.py
diff --git a/telethon/tl/custom/button.py b/telethon/_tl/custom/button.py
similarity index 100%
rename from telethon/tl/custom/button.py
rename to telethon/_tl/custom/button.py
diff --git a/telethon/tl/custom/chatgetter.py b/telethon/_tl/custom/chatgetter.py
similarity index 100%
rename from telethon/tl/custom/chatgetter.py
rename to telethon/_tl/custom/chatgetter.py
diff --git a/telethon/tl/custom/dialog.py b/telethon/_tl/custom/dialog.py
similarity index 100%
rename from telethon/tl/custom/dialog.py
rename to telethon/_tl/custom/dialog.py
diff --git a/telethon/tl/custom/draft.py b/telethon/_tl/custom/draft.py
similarity index 100%
rename from telethon/tl/custom/draft.py
rename to telethon/_tl/custom/draft.py
diff --git a/telethon/tl/custom/file.py b/telethon/_tl/custom/file.py
similarity index 100%
rename from telethon/tl/custom/file.py
rename to telethon/_tl/custom/file.py
diff --git a/telethon/tl/custom/forward.py b/telethon/_tl/custom/forward.py
similarity index 100%
rename from telethon/tl/custom/forward.py
rename to telethon/_tl/custom/forward.py
diff --git a/telethon/tl/custom/inlinebuilder.py b/telethon/_tl/custom/inlinebuilder.py
similarity index 100%
rename from telethon/tl/custom/inlinebuilder.py
rename to telethon/_tl/custom/inlinebuilder.py
diff --git a/telethon/tl/custom/inlineresult.py b/telethon/_tl/custom/inlineresult.py
similarity index 100%
rename from telethon/tl/custom/inlineresult.py
rename to telethon/_tl/custom/inlineresult.py
diff --git a/telethon/tl/custom/inlineresults.py b/telethon/_tl/custom/inlineresults.py
similarity index 100%
rename from telethon/tl/custom/inlineresults.py
rename to telethon/_tl/custom/inlineresults.py
diff --git a/telethon/tl/custom/inputsizedfile.py b/telethon/_tl/custom/inputsizedfile.py
similarity index 100%
rename from telethon/tl/custom/inputsizedfile.py
rename to telethon/_tl/custom/inputsizedfile.py
diff --git a/telethon/tl/custom/message.py b/telethon/_tl/custom/message.py
similarity index 100%
rename from telethon/tl/custom/message.py
rename to telethon/_tl/custom/message.py
diff --git a/telethon/tl/custom/messagebutton.py b/telethon/_tl/custom/messagebutton.py
similarity index 100%
rename from telethon/tl/custom/messagebutton.py
rename to telethon/_tl/custom/messagebutton.py
diff --git a/telethon/tl/custom/participantpermissions.py b/telethon/_tl/custom/participantpermissions.py
similarity index 100%
rename from telethon/tl/custom/participantpermissions.py
rename to telethon/_tl/custom/participantpermissions.py
diff --git a/telethon/tl/custom/qrlogin.py b/telethon/_tl/custom/qrlogin.py
similarity index 100%
rename from telethon/tl/custom/qrlogin.py
rename to telethon/_tl/custom/qrlogin.py
diff --git a/telethon/tl/custom/sendergetter.py b/telethon/_tl/custom/sendergetter.py
similarity index 100%
rename from telethon/tl/custom/sendergetter.py
rename to telethon/_tl/custom/sendergetter.py
diff --git a/telethon/tl/patched/__init__.py b/telethon/_tl/patched/__init__.py
similarity index 100%
rename from telethon/tl/patched/__init__.py
rename to telethon/_tl/patched/__init__.py
diff --git a/telethon/tl/tlobject.py b/telethon/_tl/tlobject.py
similarity index 100%
rename from telethon/tl/tlobject.py
rename to telethon/_tl/tlobject.py

From d48649602b92de1f4036f2246907d282f60959bf Mon Sep 17 00:00:00 2001
From: Lonami Exo 
Date: Sun, 12 Sep 2021 12:16:02 +0200
Subject: [PATCH 040/256] Replace most raw API usage with new location

---
 .gitignore                                    |   4 +-
 setup.py                                      |   2 +-
 telethon/__init__.py                          |   5 +-
 telethon/_client/account.py                   |  11 +-
 telethon/_client/auth.py                      |  45 +-
 telethon/_client/bots.py                      |  11 +-
 telethon/_client/buttons.py                   |  17 +-
 telethon/_client/chats.py                     | 187 ++++----
 telethon/_client/dialogs.py                   |  41 +-
 telethon/_client/downloads.py                 |  85 ++--
 telethon/_client/messageparse.py              |  31 +-
 telethon/_client/messages.py                  | 115 +++--
 telethon/_client/telegrambaseclient.py        |  30 +-
 telethon/_client/telegramclient.py            |  75 ++--
 telethon/_client/updates.py                   |  49 ++-
 telethon/_client/uploads.py                   |  49 ++-
 telethon/_client/users.py                     |  65 ++-
 telethon/_crypto/cdndecrypter.py              |  13 +-
 telethon/_crypto/rsa.py                       |   6 +-
 telethon/_misc/binaryreader.py                |   7 +-
 telethon/_misc/entitycache.py                 |  15 +-
 telethon/_misc/hints.py                       |  25 +-
 telethon/_misc/html.py                        |  61 ++-
 telethon/_misc/markdown.py                    |  35 +-
 telethon/_misc/password.py                    |  12 +-
 telethon/_misc/statecache.py                  |  72 ++--
 telethon/_misc/utils.py                       | 408 +++++++++---------
 telethon/_network/authenticator.py            |  33 +-
 telethon/_tl/__init__.py                      |   1 -
 telethon/_tl/custom/dialog.py                 |   2 +-
 telethon/_tl/custom/draft.py                  |  14 +-
 telethon/_tl/custom/inlinebuilder.py          |   4 +-
 telethon/_tl/custom/inlineresult.py           |   2 +-
 telethon/_tl/custom/message.py                |   2 +-
 telethon/_tl/custom/messagebutton.py          |   8 +-
 telethon/_tl/custom/qrlogin.py                |   4 +-
 telethon/errors/common.py                     |   4 +-
 telethon/errors/rpcbaseerrors.py              |  16 +-
 telethon/events/album.py                      |  18 +-
 telethon/events/callbackquery.py              |  20 +-
 telethon/events/chataction.py                 |  55 ++-
 telethon/events/common.py                     |  18 +-
 telethon/events/inlinequery.py                |  16 +-
 telethon/events/messagedeleted.py             |   8 +-
 telethon/events/messageedited.py              |   6 +-
 telethon/events/messageread.py                |  21 +-
 telethon/events/newmessage.py                 |  23 +-
 telethon/events/userupdate.py                 |  84 ++--
 telethon/sessions/memory.py                   |  42 +-
 telethon/sessions/sqlite.py                   |  16 +-
 telethon_generator/generators/tlobject.py     |   4 +-
 .../parsers/tlobject/tlobject.py              |   3 +-
 tests/telethon/tl/test_serialization.py       |   2 +-
 53 files changed, 918 insertions(+), 984 deletions(-)
 delete mode 100644 telethon/_tl/__init__.py

diff --git a/.gitignore b/.gitignore
index e81bec11..65fabceb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,6 @@
 # Generated code
-/telethon/_tl/functions/
-/telethon/_tl/types/
+/telethon/_tl/fn/
+/telethon/_tl/*.py
 /telethon/_tl/alltlobjects.py
 /telethon/errors/rpcerrorlist.py
 
diff --git a/setup.py b/setup.py
index a498980a..2cb63901 100755
--- a/setup.py
+++ b/setup.py
@@ -55,7 +55,7 @@ METHODS_IN = GENERATOR_DIR / 'data/methods.csv'
 FRIENDLY_IN = GENERATOR_DIR / 'data/friendly.csv'
 
 TLOBJECT_IN_TLS = [Path(x) for x in GENERATOR_DIR.glob('data/*.tl')]
-TLOBJECT_OUT = LIBRARY_DIR / 'tl'
+TLOBJECT_OUT = LIBRARY_DIR / '_tl'
 IMPORT_DEPTH = 2
 
 DOCS_IN_RES = GENERATOR_DIR / 'data/html'
diff --git a/telethon/__init__.py b/telethon/__init__.py
index d4e4c2c8..335abab6 100644
--- a/telethon/__init__.py
+++ b/telethon/__init__.py
@@ -1,8 +1,7 @@
 from ._client.telegramclient import TelegramClient
 from .network import connection
-from .tl import types, functions, custom
-from .tl.custom import Button
-from .tl import patched as _  # import for its side-effects
+from ._tl import custom
+from ._tl.custom import Button
 from . import version, events, utils, errors
 
 __version__ = version.__version__
diff --git a/telethon/_client/account.py b/telethon/_client/account.py
index 46e0b6dc..0331a195 100644
--- a/telethon/_client/account.py
+++ b/telethon/_client/account.py
@@ -3,8 +3,7 @@ import inspect
 import typing
 
 from .users import _NOT_A_REQUEST
-from .. import helpers, utils
-from ..tl import functions, TLRequest
+from .. import helpers, utils, _tl
 
 if typing.TYPE_CHECKING:
     from .telegramclient import TelegramClient
@@ -50,7 +49,7 @@ class _TakeoutClient:
             self.__success = exc_type is None
 
         if self.__success is not None:
-            result = await self(functions.account.FinishTakeoutSessionRequest(
+            result = await self(_tl.fn.account.FinishTakeoutSession(
                 self.__success))
             if not result:
                 raise ValueError("Failed to finish the takeout.")
@@ -66,10 +65,10 @@ class _TakeoutClient:
         requests = ((request,) if single else request)
         wrapped = []
         for r in requests:
-            if not isinstance(r, TLRequest):
+            if not isinstance(r, _tl.TLRequest):
                 raise _NOT_A_REQUEST()
             await r.resolve(self, utils)
-            wrapped.append(functions.InvokeWithTakeoutRequest(takeout_id, r))
+            wrapped.append(_tl.fn.InvokeWithTakeout(takeout_id, r))
 
         return await self.__client(
             wrapped[0] if single else wrapped, ordered=ordered)
@@ -127,7 +126,7 @@ def takeout(
     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 = _tl.fn.account.InitTakeoutSession(
             **request_kwargs)
     else:
         request = None
diff --git a/telethon/_client/auth.py b/telethon/_client/auth.py
index 3699d795..859a4e87 100644
--- a/telethon/_client/auth.py
+++ b/telethon/_client/auth.py
@@ -5,8 +5,7 @@ import sys
 import typing
 import warnings
 
-from .. import utils, helpers, errors, password as pwd_mod
-from ..tl import types, functions, custom
+from .. import utils, helpers, errors, password as pwd_mod, _tl
 
 if typing.TYPE_CHECKING:
     from .telegramclient import TelegramClient
@@ -201,7 +200,7 @@ async def sign_in(
         *,
         password: str = None,
         bot_token: str = None,
-        phone_code_hash: str = None) -> 'typing.Union[types.User, types.auth.SentCode]':
+        phone_code_hash: str = None) -> 'typing.Union[_tl.User, _tl.auth.SentCode]':
     me = await self.get_me()
     if me:
         return me
@@ -214,16 +213,16 @@ async def sign_in(
 
         # May raise PhoneCodeEmptyError, PhoneCodeExpiredError,
         # PhoneCodeHashEmptyError or PhoneCodeInvalidError.
-        request = functions.auth.SignInRequest(
+        request = _tl.fn.auth.SignIn(
             phone, phone_code_hash, str(code)
         )
     elif password:
-        pwd = await self(functions.account.GetPasswordRequest())
-        request = functions.auth.CheckPasswordRequest(
+        pwd = await self(_tl.fn.account.GetPassword())
+        request = _tl.fn.auth.CheckPassword(
             pwd_mod.compute_check(pwd, password)
         )
     elif bot_token:
-        request = functions.auth.ImportBotAuthorizationRequest(
+        request = _tl.fn.auth.ImportBotAuthorization(
             flags=0, bot_auth_token=bot_token,
             api_id=self.api_id, api_hash=self.api_hash
         )
@@ -234,7 +233,7 @@ async def sign_in(
         )
 
     result = await self(request)
-    if isinstance(result, types.auth.AuthorizationSignUpRequired):
+    if isinstance(result, _tl.auth.AuthorizationSignUpRequired):
         # Emulate pre-layer 104 behaviour
         self._tos = result.terms_of_service
         raise errors.PhoneNumberUnoccupiedError(request=request)
@@ -248,7 +247,7 @@ async def sign_up(
         last_name: str = '',
         *,
         phone: str = None,
-        phone_code_hash: str = None) -> 'types.User':
+        phone_code_hash: str = None) -> '_tl.User':
     me = await self.get_me()
     if me:
         return me
@@ -281,7 +280,7 @@ async def sign_up(
     phone, phone_code_hash = \
         self._parse_phone_and_hash(phone, phone_code_hash)
 
-    result = await self(functions.auth.SignUpRequest(
+    result = await self(_tl.fn.auth.SignUp(
         phone_number=phone,
         phone_code_hash=phone_code_hash,
         first_name=first_name,
@@ -290,7 +289,7 @@ async def sign_up(
 
     if self._tos:
         await self(
-            functions.help.AcceptTermsOfServiceRequest(self._tos.id))
+            _tl.fn.help.AcceptTermsOfService(self._tos.id))
 
     return self._on_login(result.user)
 
@@ -309,20 +308,20 @@ async def send_code_request(
         self: 'TelegramClient',
         phone: str,
         *,
-        force_sms: bool = False) -> 'types.auth.SentCode':
+        force_sms: bool = False) -> '_tl.auth.SentCode':
     result = None
     phone = utils.parse_phone(phone) or self._phone
     phone_hash = self._phone_code_hash.get(phone)
 
     if not phone_hash:
         try:
-            result = await self(functions.auth.SendCodeRequest(
-                phone, self.api_id, self.api_hash, types.CodeSettings()))
+            result = await self(_tl.fn.auth.SendCode(
+                phone, self.api_id, self.api_hash, _tl.CodeSettings()))
         except errors.AuthRestartError:
             return await self.send_code_request(phone, force_sms=force_sms)
 
         # If we already sent a SMS, do not resend the code (hash may be empty)
-        if isinstance(result.type, types.auth.SentCodeTypeSms):
+        if isinstance(result.type, _tl.auth.SentCodeTypeSms):
             force_sms = False
 
         # phone_code_hash may be empty, if it is, do not save it (#1283)
@@ -335,7 +334,7 @@ async def send_code_request(
 
     if force_sms:
         result = await self(
-            functions.auth.ResendCodeRequest(phone, phone_hash))
+            _tl.fn.auth.ResendCode(phone, phone_hash))
 
         self._phone_code_hash[phone] = result.phone_code_hash
 
@@ -348,7 +347,7 @@ async def qr_login(self: 'TelegramClient', ignored_ids: typing.List[int] = None)
 
 async def log_out(self: 'TelegramClient') -> bool:
     try:
-        await self(functions.auth.LogOutRequest())
+        await self(_tl.fn.auth.LogOut())
     except errors.RPCError:
         return False
 
@@ -375,16 +374,16 @@ async def edit_2fa(
     if email and not callable(email_code_callback):
         raise ValueError('email present without email_code_callback')
 
-    pwd = await self(functions.account.GetPasswordRequest())
+    pwd = await self(_tl.fn.account.GetPassword())
     pwd.new_algo.salt1 += os.urandom(32)
-    assert isinstance(pwd, types.account.Password)
+    assert isinstance(pwd, _tl.account.Password)
     if not pwd.has_password and current_password:
         current_password = None
 
     if current_password:
         password = pwd_mod.compute_check(pwd, current_password)
     else:
-        password = types.InputCheckPasswordEmpty()
+        password = _tl.InputCheckPasswordEmpty()
 
     if new_password:
         new_password_hash = pwd_mod.compute_digest(
@@ -393,9 +392,9 @@ async def edit_2fa(
         new_password_hash = b''
 
     try:
-        await self(functions.account.UpdatePasswordSettingsRequest(
+        await self(_tl.fn.account.UpdatePasswordSettings(
             password=password,
-            new_settings=types.account.PasswordInputSettings(
+            new_settings=_tl.account.PasswordInputSettings(
                 new_algo=pwd.new_algo,
                 new_password_hash=new_password_hash,
                 hint=hint,
@@ -409,6 +408,6 @@ async def edit_2fa(
             code = await code
 
         code = str(code)
-        await self(functions.account.ConfirmPasswordEmailRequest(code))
+        await self(_tl.fn.account.ConfirmPasswordEmail(code))
 
     return True
diff --git a/telethon/_client/bots.py b/telethon/_client/bots.py
index 0912fc20..0e967ed9 100644
--- a/telethon/_client/bots.py
+++ b/telethon/_client/bots.py
@@ -1,7 +1,6 @@
 import typing
 
-from .. import hints
-from ..tl import types, functions, custom
+from .. import hints, _tl
 
 if typing.TYPE_CHECKING:
     from .telegramclient import TelegramClient
@@ -14,14 +13,14 @@ async def inline_query(
         *,
         entity: 'hints.EntityLike' = None,
         offset: str = None,
-        geo_point: 'types.GeoPoint' = None) -> custom.InlineResults:
+        geo_point: '_tl.GeoPoint' = None) -> _tl.custom.InlineResults:
     bot = await self.get_input_entity(bot)
     if entity:
         peer = await self.get_input_entity(entity)
     else:
-        peer = types.InputPeerEmpty()
+        peer = _tl.InputPeerEmpty()
 
-    result = await self(functions.messages.GetInlineBotResultsRequest(
+    result = await self(_tl.fn.messages.GetInlineBotResults(
         bot=bot,
         peer=peer,
         query=query,
@@ -29,4 +28,4 @@ async def inline_query(
         geo_point=geo_point
     ))
 
-    return custom.InlineResults(self, result, entity=peer if entity else None)
+    return _tl.custom.InlineResults(self, result, entity=peer if entity else None)
diff --git a/telethon/_client/buttons.py b/telethon/_client/buttons.py
index 41413708..5dd9c413 100644
--- a/telethon/_client/buttons.py
+++ b/telethon/_client/buttons.py
@@ -1,12 +1,11 @@
 import typing
 
-from .. import utils, hints
-from ..tl import types, custom
+from .. import utils, hints, _tl
 
 
 def build_reply_markup(
         buttons: 'typing.Optional[hints.MarkupLike]',
-        inline_only: bool = False) -> 'typing.Optional[types.TypeReplyMarkup]':
+        inline_only: bool = False) -> 'typing.Optional[_tl.TypeReplyMarkup]':
     if buttons is None:
         return None
 
@@ -31,7 +30,7 @@ def build_reply_markup(
     for row in buttons:
         current = []
         for button in row:
-            if isinstance(button, custom.Button):
+            if isinstance(button, _tl.custom.Button):
                 if button.resize is not None:
                     resize = button.resize
                 if button.single_use is not None:
@@ -40,10 +39,10 @@ def build_reply_markup(
                     selective = button.selective
 
                 button = button.button
-            elif isinstance(button, custom.MessageButton):
+            elif isinstance(button, _tl.custom.MessageButton):
                 button = button.button
 
-            inline = custom.Button._is_inline(button)
+            inline = _tl.custom.Button._is_inline(button)
             is_inline |= inline
             is_normal |= not inline
 
@@ -52,14 +51,14 @@ def build_reply_markup(
                 current.append(button)
 
         if current:
-            rows.append(types.KeyboardButtonRow(current))
+            rows.append(_tl.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)
+        return _tl.ReplyInlineMarkup(rows)
     # elif is_normal:
-    return types.ReplyKeyboardMarkup(
+    return _tl.ReplyKeyboardMarkup(
         rows, resize=resize, single_use=single_use, selective=selective)
diff --git a/telethon/_client/chats.py b/telethon/_client/chats.py
index 4147b45b..904bba10 100644
--- a/telethon/_client/chats.py
+++ b/telethon/_client/chats.py
@@ -4,9 +4,8 @@ import itertools
 import string
 import typing
 
-from .. import helpers, utils, hints, errors
+from .. import helpers, utils, hints, errors, _tl
 from ..requestiter import RequestIter
-from ..tl import types, functions, custom
 
 if typing.TYPE_CHECKING:
     from .telegramclient import TelegramClient
@@ -18,28 +17,28 @@ _MAX_PROFILE_PHOTO_CHUNK_SIZE = 100
 
 class _ChatAction:
     _str_mapping = {
-        'typing': types.SendMessageTypingAction(),
-        'contact': types.SendMessageChooseContactAction(),
-        'game': types.SendMessageGamePlayAction(),
-        'location': types.SendMessageGeoLocationAction(),
-        'sticker': types.SendMessageChooseStickerAction(),
+        'typing': _tl.SendMessageTypingAction(),
+        'contact': _tl.SendMessageChooseContactAction(),
+        'game': _tl.SendMessageGamePlayAction(),
+        'location': _tl.SendMessageGeoLocationAction(),
+        'sticker': _tl.SendMessageChooseStickerAction(),
 
-        'record-audio': types.SendMessageRecordAudioAction(),
-        'record-voice': types.SendMessageRecordAudioAction(),  # alias
-        'record-round': types.SendMessageRecordRoundAction(),
-        'record-video': types.SendMessageRecordVideoAction(),
+        'record-audio': _tl.SendMessageRecordAudioAction(),
+        'record-voice': _tl.SendMessageRecordAudioAction(),  # alias
+        'record-round': _tl.SendMessageRecordRoundAction(),
+        'record-video': _tl.SendMessageRecordVideoAction(),
 
-        'audio': types.SendMessageUploadAudioAction(1),
-        'voice': types.SendMessageUploadAudioAction(1),  # alias
-        'song': types.SendMessageUploadAudioAction(1),  # alias
-        'round': types.SendMessageUploadRoundAction(1),
-        'video': types.SendMessageUploadVideoAction(1),
+        'audio': _tl.SendMessageUploadAudioAction(1),
+        'voice': _tl.SendMessageUploadAudioAction(1),  # alias
+        'song': _tl.SendMessageUploadAudioAction(1),  # alias
+        'round': _tl.SendMessageUploadRoundAction(1),
+        'video': _tl.SendMessageUploadVideoAction(1),
 
-        'photo': types.SendMessageUploadPhotoAction(1),
-        'document': types.SendMessageUploadDocumentAction(1),
-        'file': types.SendMessageUploadDocumentAction(1),  # alias
+        'photo': _tl.SendMessageUploadPhotoAction(1),
+        'document': _tl.SendMessageUploadDocumentAction(1),
+        'file': _tl.SendMessageUploadDocumentAction(1),  # alias
 
-        'cancel': types.SendMessageCancelAction()
+        'cancel': _tl.SendMessageCancelAction()
     }
 
     def __init__(self, client, chat, action, *, delay, auto_cancel):
@@ -58,7 +57,7 @@ class _ChatAction:
         # Since `self._action` is passed by reference we can avoid
         # recreating the request all the time and still modify
         # `self._action.progress` directly in `progress`.
-        self._request = functions.messages.SetTypingRequest(
+        self._request = _tl.fn.messages.SetTyping(
             self._chat, self._action)
 
         self._running = True
@@ -85,8 +84,8 @@ class _ChatAction:
             pass
         except asyncio.CancelledError:
             if self._auto_cancel:
-                await self._client(functions.messages.SetTypingRequest(
-                    self._chat, types.SendMessageCancelAction()))
+                await self._client(_tl.fn.messages.SetTyping(
+                    self._chat, _tl.SendMessageCancelAction()))
 
     def progress(self, current, total):
         if hasattr(self._action, 'progress'):
@@ -96,10 +95,10 @@ class _ChatAction:
 class _ParticipantsIter(RequestIter):
     async def _init(self, entity, filter, search, aggressive):
         if isinstance(filter, type):
-            if filter in (types.ChannelParticipantsBanned,
-                          types.ChannelParticipantsKicked,
-                          types.ChannelParticipantsSearch,
-                          types.ChannelParticipantsContacts):
+            if filter in (_tl.ChannelParticipantsBanned,
+                          _tl.ChannelParticipantsKicked,
+                          _tl.ChannelParticipantsSearch,
+                          _tl.ChannelParticipantsContacts):
                 # These require a `q` parameter (support types for convenience)
                 filter = filter('')
             else:
@@ -125,23 +124,23 @@ class _ParticipantsIter(RequestIter):
             if self.limit <= 0:
                 # May not have access to the channel, but getFull can get the .total.
                 self.total = (await self.client(
-                    functions.channels.GetFullChannelRequest(entity)
+                    _tl.fn.channels.GetFullChannel(entity)
                 )).full_chat.participants_count
                 raise StopAsyncIteration
 
             self.seen = set()
             if aggressive and not filter:
-                self.requests.extend(functions.channels.GetParticipantsRequest(
+                self.requests.extend(_tl.fn.channels.GetParticipants(
                     channel=entity,
-                    filter=types.ChannelParticipantsSearch(x),
+                    filter=_tl.ChannelParticipantsSearch(x),
                     offset=0,
                     limit=_MAX_PARTICIPANTS_CHUNK_SIZE,
                     hash=0
                 ) for x in (search or string.ascii_lowercase))
             else:
-                self.requests.append(functions.channels.GetParticipantsRequest(
+                self.requests.append(_tl.fn.channels.GetParticipants(
                     channel=entity,
-                    filter=filter or types.ChannelParticipantsSearch(search),
+                    filter=filter or _tl.ChannelParticipantsSearch(search),
                     offset=0,
                     limit=_MAX_PARTICIPANTS_CHUNK_SIZE,
                     hash=0
@@ -149,9 +148,9 @@ class _ParticipantsIter(RequestIter):
 
         elif ty == helpers._EntityType.CHAT:
             full = await self.client(
-                functions.messages.GetFullChatRequest(entity.chat_id))
+                _tl.fn.messages.GetFullChat(entity.chat_id))
             if not isinstance(
-                    full.full_chat.participants, types.ChatParticipants):
+                    full.full_chat.participants, _tl.ChatParticipants):
                 # ChatParticipantsForbidden won't have ``.participants``
                 self.total = 0
                 raise StopAsyncIteration
@@ -160,7 +159,7 @@ class _ParticipantsIter(RequestIter):
 
             users = {user.id: user for user in full.users}
             for participant in full.full_chat.participants.participants:
-                if isinstance(participant, types.ChannelParticipantBanned):
+                if isinstance(participant, _tl.ChannelParticipantBanned):
                     user_id = participant.peer.user_id
                 else:
                     user_id = participant.user_id
@@ -202,15 +201,15 @@ class _ParticipantsIter(RequestIter):
         if self.total is None:
             f = self.requests[0].filter
             if len(self.requests) > 1 or (
-                not isinstance(f, types.ChannelParticipantsRecent)
-                and (not isinstance(f, types.ChannelParticipantsSearch) or f.q)
+                not isinstance(f, _tl.ChannelParticipantsRecent)
+                and (not isinstance(f, _tl.ChannelParticipantsSearch) or f.q)
             ):
                 # Only do an additional getParticipants here to get the total
                 # if there's a filter which would reduce the real total number.
                 # getParticipants is cheaper than getFull.
-                self.total = (await self.client(functions.channels.GetParticipantsRequest(
+                self.total = (await self.client(_tl.fn.channels.GetParticipants(
                     channel=self.requests[0].channel,
-                    filter=types.ChannelParticipantsRecent(),
+                    filter=_tl.ChannelParticipantsRecent(),
                     offset=0,
                     limit=1,
                     hash=0
@@ -230,8 +229,8 @@ class _ParticipantsIter(RequestIter):
             users = {user.id: user for user in participants.users}
             for participant in participants.participants:
 
-                if isinstance(participant, types.ChannelParticipantBanned):
-                    if not isinstance(participant.peer, types.PeerUser):
+                if isinstance(participant, _tl.ChannelParticipantBanned):
+                    if not isinstance(participant.peer, _tl.PeerUser):
                         # May have the entire channel banned. See #3105.
                         continue
                     user_id = participant.peer.user_id
@@ -257,7 +256,7 @@ class _AdminLogIter(RequestIter):
         if any((join, leave, invite, restrict, unrestrict, ban, unban,
                 promote, demote, info, settings, pinned, edit, delete,
                 group_call)):
-            events_filter = types.ChannelAdminLogEventsFilter(
+            events_filter = _tl.ChannelAdminLogEventsFilter(
                 join=join, leave=leave, invite=invite, ban=restrict,
                 unban=unrestrict, kick=ban, unkick=unban, promote=promote,
                 demote=demote, info=info, settings=settings, pinned=pinned,
@@ -276,7 +275,7 @@ class _AdminLogIter(RequestIter):
             for admin in admins:
                 admin_list.append(await self.client.get_input_entity(admin))
 
-        self.request = functions.channels.GetAdminLogRequest(
+        self.request = _tl.fn.channels.GetAdminLog(
             self.entity, q=search or '', min_id=min_id, max_id=max_id,
             limit=0, events_filter=events_filter, admins=admin_list or None
         )
@@ -290,7 +289,7 @@ class _AdminLogIter(RequestIter):
         self.request.max_id = min((e.id for e in r.events), default=0)
         for ev in r.events:
             if isinstance(ev.action,
-                          types.ChannelAdminLogEventActionEditMessage):
+                          _tl.ChannelAdminLogEventActionEditMessage):
                 ev.action.prev_message._finish_init(
                     self.client, entities, self.entity)
 
@@ -298,11 +297,11 @@ class _AdminLogIter(RequestIter):
                     self.client, entities, self.entity)
 
             elif isinstance(ev.action,
-                            types.ChannelAdminLogEventActionDeleteMessage):
+                            _tl.ChannelAdminLogEventActionDeleteMessage):
                 ev.action.message._finish_init(
                     self.client, entities, self.entity)
 
-            self.buffer.append(custom.AdminLogEvent(ev, entities))
+            self.buffer.append(_tl.custom.AdminLogEvent(ev, entities))
 
         if len(r.events) < self.request.limit:
             return True
@@ -315,17 +314,17 @@ class _ProfilePhotoIter(RequestIter):
         entity = await self.client.get_input_entity(entity)
         ty = helpers._entity_type(entity)
         if ty == helpers._EntityType.USER:
-            self.request = functions.photos.GetUserPhotosRequest(
+            self.request = _tl.fn.photos.GetUserPhotos(
                 entity,
                 offset=offset,
                 max_id=max_id,
                 limit=1
             )
         else:
-            self.request = functions.messages.SearchRequest(
+            self.request = _tl.fn.messages.Search(
                 peer=entity,
                 q='',
-                filter=types.InputMessagesFilterChatPhotos(),
+                filter=_tl.InputMessagesFilterChatPhotos(),
                 min_date=None,
                 max_date=None,
                 offset_id=0,
@@ -339,9 +338,9 @@ class _ProfilePhotoIter(RequestIter):
         if self.limit == 0:
             self.request.limit = 1
             result = await self.client(self.request)
-            if isinstance(result, types.photos.Photos):
+            if isinstance(result, _tl.photos.Photos):
                 self.total = len(result.photos)
-            elif isinstance(result, types.messages.Messages):
+            elif isinstance(result, _tl.messages.Messages):
                 self.total = len(result.messages)
             else:
                 # Luckily both photosSlice and messages have a count for total
@@ -351,17 +350,17 @@ class _ProfilePhotoIter(RequestIter):
         self.request.limit = min(self.left, _MAX_PROFILE_PHOTO_CHUNK_SIZE)
         result = await self.client(self.request)
 
-        if isinstance(result, types.photos.Photos):
+        if isinstance(result, _tl.photos.Photos):
             self.buffer = result.photos
             self.left = len(self.buffer)
             self.total = len(self.buffer)
-        elif isinstance(result, types.messages.Messages):
+        elif isinstance(result, _tl.messages.Messages):
             self.buffer = [x.action.photo for x in result.messages
-                           if isinstance(x.action, types.MessageActionChatEditPhoto)]
+                           if isinstance(x.action, _tl.MessageActionChatEditPhoto)]
 
             self.left = len(self.buffer)
             self.total = len(self.buffer)
-        elif isinstance(result, types.photos.PhotosSlice):
+        elif isinstance(result, _tl.photos.PhotosSlice):
             self.buffer = result.photos
             self.total = result.count
             if len(self.buffer) < self.request.limit:
@@ -381,16 +380,16 @@ class _ProfilePhotoIter(RequestIter):
             # Unconditionally fetch the full channel to obtain this photo and
             # yield it with the rest (unless it's a duplicate).
             seen_id = None
-            if isinstance(result, types.messages.ChannelMessages):
-                channel = await self.client(functions.channels.GetFullChannelRequest(self.request.peer))
+            if isinstance(result, _tl.messages.ChannelMessages):
+                channel = await self.client(_tl.fn.channels.GetFullChannel(self.request.peer))
                 photo = channel.full_chat.chat_photo
-                if isinstance(photo, types.Photo):
+                if isinstance(photo, _tl.Photo):
                     self.buffer.append(photo)
                     seen_id = photo.id
 
             self.buffer.extend(
                 x.action.photo for x in result.messages
-                if isinstance(x.action, types.MessageActionChatEditPhoto)
+                if isinstance(x.action, _tl.MessageActionChatEditPhoto)
                 and x.action.photo.id != seen_id
             )
 
@@ -407,7 +406,7 @@ def iter_participants(
         limit: float = None,
         *,
         search: str = '',
-        filter: 'types.TypeChannelParticipantsFilter' = None,
+        filter: '_tl.TypeChannelParticipantsFilter' = None,
         aggressive: bool = False) -> _ParticipantsIter:
     return _ParticipantsIter(
         self,
@@ -506,7 +505,7 @@ async def get_profile_photos(
 def action(
         self: 'TelegramClient',
         entity: 'hints.EntityLike',
-        action: 'typing.Union[str, types.TypeSendMessageAction]',
+        action: 'typing.Union[str, _tl.TypeSendMessageAction]',
         *,
         delay: float = 4,
         auto_cancel: bool = True) -> 'typing.Union[_ChatAction, typing.Coroutine]':
@@ -516,17 +515,17 @@ def action(
         except KeyError:
             raise ValueError(
                 'No such action "{}"'.format(action)) from None
-    elif not isinstance(action, types.TLObject) or action.SUBCLASS_OF_ID != 0x20b2cc21:
+    elif not isinstance(action, _tl.TLObject) or action.SUBCLASS_OF_ID != 0x20b2cc21:
         # 0x20b2cc21 = crc32(b'SendMessageAction')
         if isinstance(action, type):
             raise ValueError('You must pass an instance, not the class')
         else:
             raise ValueError('Cannot use {} as action'.format(action))
 
-    if isinstance(action, types.SendMessageCancelAction):
+    if isinstance(action, _tl.SendMessageCancelAction):
         # ``SetTypingRequest.resolve`` will get input peer of ``entity``.
-        return self(functions.messages.SetTypingRequest(
-            entity, types.SendMessageCancelAction()))
+        return self(_tl.fn.messages.SetTyping(
+            entity, _tl.SendMessageCancelAction()))
 
     return _ChatAction(
         self, entity, action, delay=delay, auto_cancel=auto_cancel)
@@ -547,7 +546,7 @@ async def edit_admin(
         manage_call: bool = None,
         anonymous: bool = None,
         is_admin: bool = None,
-        title: str = None) -> types.Updates:
+        title: str = None) -> _tl.Updates:
     entity = await self.get_input_entity(entity)
     user = await self.get_input_entity(user)
     ty = helpers._entity_type(user)
@@ -576,7 +575,7 @@ async def edit_admin(
                 edit_messages = None
 
         perms = locals()
-        return await self(functions.channels.EditAdminRequest(entity, user, types.ChatAdminRights(**{
+        return await self(_tl.fn.channels.EditAdmin(entity, user, _tl.ChatAdminRights(**{
             # A permission is its explicit (not-None) value or `is_admin`.
             # This essentially makes `is_admin` be the default value.
             name: perms[name] if perms[name] is not None else is_admin
@@ -589,7 +588,7 @@ async def edit_admin(
         if is_admin is None:
             is_admin = any(locals()[x] for x in perm_names)
 
-        return await self(functions.messages.EditChatAdminRequest(
+        return await self(_tl.fn.messages.EditChatAdmin(
             entity, user, is_admin=is_admin))
 
     else:
@@ -613,13 +612,13 @@ async def edit_permissions(
         send_polls: bool = True,
         change_info: bool = True,
         invite_users: bool = True,
-        pin_messages: bool = True) -> types.Updates:
+        pin_messages: bool = True) -> _tl.Updates:
     entity = await self.get_input_entity(entity)
     ty = helpers._entity_type(entity)
     if ty != helpers._EntityType.CHANNEL:
         raise ValueError('You must pass either a channel or a supergroup')
 
-    rights = types.ChatBannedRights(
+    rights = _tl.ChatBannedRights(
         until_date=until_date,
         view_messages=not view_messages,
         send_messages=not send_messages,
@@ -636,7 +635,7 @@ async def edit_permissions(
     )
 
     if user is None:
-        return await self(functions.messages.EditChatDefaultBannedRightsRequest(
+        return await self(_tl.fn.messages.EditChatDefaultBannedRights(
             peer=entity,
             banned_rights=rights
         ))
@@ -646,10 +645,10 @@ async def edit_permissions(
     if ty != helpers._EntityType.USER:
         raise ValueError('You must pass a user entity')
 
-    if isinstance(user, types.InputPeerSelf):
+    if isinstance(user, _tl.InputPeerSelf):
         raise ValueError('You cannot restrict yourself')
 
-    return await self(functions.channels.EditBannedRequest(
+    return await self(_tl.fn.channels.EditBanned(
         channel=entity,
         participant=user,
         banned_rights=rights
@@ -667,24 +666,24 @@ async def kick_participant(
 
     ty = helpers._entity_type(entity)
     if ty == helpers._EntityType.CHAT:
-        resp = await self(functions.messages.DeleteChatUserRequest(entity.chat_id, user))
+        resp = await self(_tl.fn.messages.DeleteChatUser(entity.chat_id, user))
     elif ty == helpers._EntityType.CHANNEL:
-        if isinstance(user, types.InputPeerSelf):
+        if isinstance(user, _tl.InputPeerSelf):
             # Despite no longer being in the channel, the account still
             # seems to get the service message.
-            resp = await self(functions.channels.LeaveChannelRequest(entity))
+            resp = await self(_tl.fn.channels.LeaveChannel(entity))
         else:
-            resp = await self(functions.channels.EditBannedRequest(
+            resp = await self(_tl.fn.channels.EditBanned(
                 channel=entity,
                 participant=user,
-                banned_rights=types.ChatBannedRights(
+                banned_rights=_tl.ChatBannedRights(
                     until_date=None, view_messages=True)
             ))
             await asyncio.sleep(0.5)
-            await self(functions.channels.EditBannedRequest(
+            await self(_tl.fn.channels.EditBanned(
                 channel=entity,
                 participant=user,
-                banned_rights=types.ChatBannedRights(until_date=None)
+                banned_rights=_tl.ChatBannedRights(until_date=None)
             ))
     else:
         raise ValueError('You must pass either a channel or a chat')
@@ -695,14 +694,14 @@ async def get_permissions(
         self: 'TelegramClient',
         entity: 'hints.EntityLike',
         user: 'hints.EntityLike' = None
-) -> 'typing.Optional[custom.ParticipantPermissions]':
+) -> 'typing.Optional[_tl.custom.ParticipantPermissions]':
     entity = await self.get_entity(entity)
 
     if not user:
-        if isinstance(entity, types.Channel):
-            FullChat = await self(functions.channels.GetFullChannelRequest(entity))
-        elif isinstance(entity, types.Chat):
-            FullChat = await self(functions.messages.GetFullChatRequest(entity))
+        if isinstance(entity, _tl.Channel):
+            FullChat = await self(_tl.fn.channels.GetFullChannel(entity))
+        elif isinstance(entity, _tl.Chat):
+            FullChat = await self(_tl.fn.messages.GetFullChat(entity))
         else:
             return
         return FullChat.chats[0].default_banned_rights
@@ -712,20 +711,20 @@ async def get_permissions(
     if helpers._entity_type(user) != helpers._EntityType.USER:
         raise ValueError('You must pass a user entity')
     if helpers._entity_type(entity) == helpers._EntityType.CHANNEL:
-        participant = await self(functions.channels.GetParticipantRequest(
+        participant = await self(_tl.fn.channels.GetParticipant(
             entity,
             user
         ))
-        return custom.ParticipantPermissions(participant.participant, False)
+        return _tl.custom.ParticipantPermissions(participant.participant, False)
     elif helpers._entity_type(entity) == helpers._EntityType.CHAT:
-        chat = await self(functions.messages.GetFullChatRequest(
+        chat = await self(_tl.fn.messages.GetFullChat(
             entity
         ))
-        if isinstance(user, types.InputPeerSelf):
+        if isinstance(user, _tl.InputPeerSelf):
             user = await self.get_me(input_peer=True)
         for participant in chat.full_chat.participants.participants:
             if participant.user_id == user.user_id:
-                return custom.ParticipantPermissions(participant, True)
+                return _tl.custom.ParticipantPermissions(participant, True)
         raise errors.UserNotParticipantError(None)
 
     raise ValueError('You must pass either a channel or a chat')
@@ -733,7 +732,7 @@ async def get_permissions(
 async def get_stats(
         self: 'TelegramClient',
         entity: 'hints.EntityLike',
-        message: 'typing.Union[int, types.Message]' = None,
+        message: 'typing.Union[int, _tl.Message]' = None,
 ):
     entity = await self.get_input_entity(entity)
     if helpers._entity_type(entity) != helpers._EntityType.CHANNEL:
@@ -742,7 +741,7 @@ async def get_stats(
     message = utils.get_message_id(message)
     if message is not None:
         try:
-            req = functions.stats.GetMessageStatsRequest(entity, message)
+            req = _tl.fn.stats.GetMessageStats(entity, message)
             return await self(req)
         except errors.StatsMigrateError as e:
             dc = e.dc
@@ -751,12 +750,12 @@ async def get_stats(
         # try to guess and if it fails we know it's the other one (best case
         # no extra request, worst just one).
         try:
-            req = functions.stats.GetBroadcastStatsRequest(entity)
+            req = _tl.fn.stats.GetBroadcastStats(entity)
             return await self(req)
         except errors.StatsMigrateError as e:
             dc = e.dc
         except errors.BroadcastRequiredError:
-            req = functions.stats.GetMegagroupStatsRequest(entity)
+            req = _tl.fn.stats.GetMegagroupStats(entity)
             try:
                 return await self(req)
             except errors.StatsMigrateError as e:
diff --git a/telethon/_client/dialogs.py b/telethon/_client/dialogs.py
index 8471c7fb..aee8861d 100644
--- a/telethon/_client/dialogs.py
+++ b/telethon/_client/dialogs.py
@@ -3,9 +3,8 @@ import inspect
 import itertools
 import typing
 
-from .. import helpers, utils, hints, errors
+from .. import helpers, utils, hints, errors, _tl
 from ..requestiter import RequestIter
-from ..tl import types, functions, custom
 
 _MAX_CHUNK_SIZE = 100
 
@@ -21,14 +20,14 @@ def _dialog_message_key(peer, message_id):
     and the peer ID is required to distinguish between them. But it is not
     necessary in small group chats and private chats.
     """
-    return (peer.channel_id if isinstance(peer, types.PeerChannel) else None), message_id
+    return (peer.channel_id if isinstance(peer, _tl.PeerChannel) else None), message_id
 
 
 class _DialogsIter(RequestIter):
     async def _init(
             self, offset_date, offset_id, offset_peer, ignore_pinned, ignore_migrated, folder
     ):
-        self.request = functions.messages.GetDialogsRequest(
+        self.request = _tl.fn.messages.GetDialogs(
             offset_date=offset_date,
             offset_id=offset_id,
             offset_peer=offset_peer,
@@ -56,7 +55,7 @@ class _DialogsIter(RequestIter):
 
         entities = {utils.get_peer_id(x): x
                     for x in itertools.chain(r.users, r.chats)
-                    if not isinstance(x, (types.UserEmpty, types.ChatEmpty))}
+                    if not isinstance(x, (_tl.UserEmpty, _tl.ChatEmpty))}
 
         messages = {}
         for m in r.messages:
@@ -80,7 +79,7 @@ class _DialogsIter(RequestIter):
                     # Real world example: https://t.me/TelethonChat/271471
                     continue
 
-                cd = custom.Dialog(self.client, d, entities, message)
+                cd = _tl.custom.Dialog(self.client, d, entities, message)
                 if cd.dialog.pts:
                     self.client._channel_pts[cd.id] = cd.dialog.pts
 
@@ -89,7 +88,7 @@ class _DialogsIter(RequestIter):
                     self.buffer.append(cd)
 
         if len(r.dialogs) < self.request.limit\
-                or not isinstance(r, types.messages.DialogsSlice):
+                or not isinstance(r, _tl.messages.DialogsSlice):
             # Less than we requested means we reached the end, or
             # we didn't get a DialogsSlice which means we got all.
             return True
@@ -112,15 +111,15 @@ class _DialogsIter(RequestIter):
 class _DraftsIter(RequestIter):
     async def _init(self, entities, **kwargs):
         if not entities:
-            r = await self.client(functions.messages.GetAllDraftsRequest())
+            r = await self.client(_tl.fn.messages.GetAllDrafts())
             items = r.updates
         else:
             peers = []
             for entity in entities:
-                peers.append(types.InputDialogPeer(
+                peers.append(_tl.InputDialogPeer(
                     await self.client.get_input_entity(entity)))
 
-            r = await self.client(functions.messages.GetPeerDialogsRequest(peers))
+            r = await self.client(_tl.fn.messages.GetPeerDialogs(peers))
             items = r.dialogs
 
         # TODO Maybe there should be a helper method for this?
@@ -128,7 +127,7 @@ class _DraftsIter(RequestIter):
                     for x in itertools.chain(r.users, r.chats)}
 
         self.buffer.extend(
-            custom.Draft(self.client, entities[utils.get_peer_id(d.peer)], d.draft)
+            _tl.custom.Draft(self.client, entities[utils.get_peer_id(d.peer)], d.draft)
             for d in items
         )
 
@@ -142,7 +141,7 @@ def iter_dialogs(
         *,
         offset_date: 'hints.DateLike' = None,
         offset_id: int = 0,
-        offset_peer: 'hints.EntityLike' = types.InputPeerEmpty(),
+        offset_peer: 'hints.EntityLike' = _tl.InputPeerEmpty(),
         ignore_pinned: bool = False,
         ignore_migrated: bool = False,
         folder: int = None,
@@ -192,12 +191,12 @@ async def edit_folder(
         folder: typing.Union[int, typing.Sequence[int]] = None,
         *,
         unpack=None
-) -> types.Updates:
+) -> _tl.Updates:
     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(
+        return await self(_tl.fn.folders.DeleteFolder(
             folder_id=unpack
         ))
 
@@ -214,8 +213,8 @@ async def edit_folder(
     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)
+    return await self(_tl.fn.folders.EditPeerFolders([
+        _tl.InputFolderPeer(x, folder_id=y)
         for x, y in zip(entities, folder)
     ]))
 
@@ -227,7 +226,7 @@ async def delete_dialog(
 ):
     # 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):
+    if isinstance(entity, _tl.Chat):
         deactivated = entity.deactivated
     else:
         deactivated = False
@@ -235,12 +234,12 @@ async def delete_dialog(
     entity = await self.get_input_entity(entity)
     ty = helpers._entity_type(entity)
     if ty == helpers._EntityType.CHANNEL:
-        return await self(functions.channels.LeaveChannelRequest(entity))
+        return await self(_tl.fn.channels.LeaveChannel(entity))
 
     if ty == helpers._EntityType.CHAT and not deactivated:
         try:
-            result = await self(functions.messages.DeleteChatUserRequest(
-                entity.chat_id, types.InputUserSelf(), revoke_history=revoke
+            result = await self(_tl.fn.messages.DeleteChatUser(
+                entity.chat_id, _tl.InputUserSelf(), revoke_history=revoke
             ))
         except errors.PeerIdInvalidError:
             # Happens if we didn't have the deactivated information
@@ -249,6 +248,6 @@ async def delete_dialog(
         result = None
 
     if not await self.is_bot():
-        await self(functions.messages.DeleteHistoryRequest(entity, 0, revoke=revoke))
+        await self(_tl.fn.messages.DeleteHistory(entity, 0, revoke=revoke))
 
     return result
diff --git a/telethon/_client/downloads.py b/telethon/_client/downloads.py
index aa8ed59a..974db2a0 100644
--- a/telethon/_client/downloads.py
+++ b/telethon/_client/downloads.py
@@ -8,9 +8,8 @@ import asyncio
 
 from ..crypto import AES
 
-from .. import utils, helpers, errors, hints
+from .. import utils, helpers, errors, hints, _tl
 from ..requestiter import RequestIter
-from ..tl import TLObject, types, functions
 
 try:
     import aiohttp
@@ -31,7 +30,7 @@ class _DirectDownloadIter(RequestIter):
     async def _init(
             self, file, dc_id, offset, stride, chunk_size, request_size, file_size, msg_data
     ):
-        self.request = functions.upload.GetFileRequest(
+        self.request = _tl.fn.upload.GetFile(
             file, offset=offset, limit=request_size)
 
         self.total = file_size
@@ -50,7 +49,7 @@ class _DirectDownloadIter(RequestIter):
                 self._sender = await self.client._borrow_exported_sender(dc_id)
             except errors.DcIdInvalidError:
                 # Can't export a sender for the ID we are currently in
-                config = await self.client(functions.help.GetConfigRequest())
+                config = await self.client(_tl.fn.help.GetConfig())
                 for option in config.dc_options:
                     if option.ip_address == self.client.session.server_address:
                         self.client.session.set_dc(
@@ -75,7 +74,7 @@ class _DirectDownloadIter(RequestIter):
         try:
             result = await self.client._call(self._sender, self.request)
             self._timed_out = False
-            if isinstance(result, types.upload.FileCdnRedirect):
+            if isinstance(result, _tl.upload.FileCdnRedirect):
                 raise NotImplementedError  # TODO Implement
             else:
                 return result.bytes
@@ -99,7 +98,7 @@ class _DirectDownloadIter(RequestIter):
         except errors.FilerefUpgradeNeededError as e:
             # Only implemented for documents which are the ones that may take that long to download
             if not self._msg_data \
-                    or not isinstance(self.request.location, types.InputDocumentFileLocation) \
+                    or not isinstance(self.request.location, _tl.InputDocumentFileLocation) \
                     or self.request.location.thumb_size != '':
                 raise
 
@@ -107,7 +106,7 @@ class _DirectDownloadIter(RequestIter):
             chat, msg_id = self._msg_data
             msg = await self.client.get_messages(chat, ids=msg_id)
 
-            if not isinstance(msg.media, types.MessageMediaDocument):
+            if not isinstance(msg.media, _tl.MessageMediaDocument):
                 raise
 
             document = msg.media.document
@@ -200,7 +199,7 @@ async def download_profile_photo(
     ENTITIES = (0x2da17977, 0xc5af5d94, 0x1f4661b9, 0xd49a2697)
     # ('InputPeer', 'InputUser', 'InputChannel')
     INPUTS = (0xc91c90b6, 0xe669bf46, 0x40f202fd)
-    if not isinstance(entity, TLObject) or entity.SUBCLASS_OF_ID in INPUTS:
+    if not isinstance(entity, _tl.TLObject) or entity.SUBCLASS_OF_ID in INPUTS:
         entity = await self.get_entity(entity)
 
     thumb = -1 if download_big else 0
@@ -225,9 +224,9 @@ async def download_profile_photo(
 
         photo = entity.photo
 
-    if isinstance(photo, (types.UserProfilePhoto, types.ChatPhoto)):
+    if isinstance(photo, (_tl.UserProfilePhoto, _tl.ChatPhoto)):
         dc_id = photo.dc_id
-        loc = types.InputPeerPhotoFileLocation(
+        loc = _tl.InputPeerPhotoFileLocation(
             peer=await self.get_input_entity(entity),
             photo_id=photo.photo_id,
             big=download_big
@@ -253,7 +252,7 @@ async def download_profile_photo(
         ie = await self.get_input_entity(entity)
         ty = helpers._entity_type(ie)
         if ty == helpers._EntityType.CHANNEL:
-            full = await self(functions.channels.GetFullChannelRequest(ie))
+            full = await self(_tl.fn.channels.GetFullChannel(ie))
             return await self._download_photo(
                 full.full_chat.chat_photo, file,
                 date=None, progress_callback=None,
@@ -268,7 +267,7 @@ async def download_media(
         message: 'hints.MessageLike',
         file: 'hints.FileLike' = None,
         *,
-        thumb: 'typing.Union[int, types.TypePhotoSize]' = None,
+        thumb: 'typing.Union[int, _tl.TypePhotoSize]' = None,
         progress_callback: 'hints.ProgressCallback' = None) -> typing.Optional[typing.Union[str, bytes]]:
     # Downloading large documents may be slow enough to require a new file reference
     # to be obtained mid-download. Store (input chat, message id) so that the message
@@ -276,7 +275,7 @@ async def download_media(
     msg_data = None
 
     # TODO This won't work for messageService
-    if isinstance(message, types.Message):
+    if isinstance(message, _tl.Message):
         date = message.date
         media = message.media
         msg_data = (message.input_chat, message.id) if message.input_chat else None
@@ -287,28 +286,28 @@ async def download_media(
     if isinstance(media, str):
         media = utils.resolve_bot_file_id(media)
 
-    if isinstance(media, types.MessageService):
+    if isinstance(media, _tl.MessageService):
         if isinstance(message.action,
-                        types.MessageActionChatEditPhoto):
+                        _tl.MessageActionChatEditPhoto):
             media = media.photo
 
-    if isinstance(media, types.MessageMediaWebPage):
-        if isinstance(media.webpage, types.WebPage):
+    if isinstance(media, _tl.MessageMediaWebPage):
+        if isinstance(media.webpage, _tl.WebPage):
             media = media.webpage.document or media.webpage.photo
 
-    if isinstance(media, (types.MessageMediaPhoto, types.Photo)):
+    if isinstance(media, (_tl.MessageMediaPhoto, _tl.Photo)):
         return await self._download_photo(
             media, file, date, thumb, progress_callback
         )
-    elif isinstance(media, (types.MessageMediaDocument, types.Document)):
+    elif isinstance(media, (_tl.MessageMediaDocument, _tl.Document)):
         return await self._download_document(
             media, file, date, thumb, progress_callback, msg_data
         )
-    elif isinstance(media, types.MessageMediaContact) and thumb is None:
+    elif isinstance(media, _tl.MessageMediaContact) and thumb is None:
         return self._download_contact(
             media, file
         )
-    elif isinstance(media, (types.WebDocument, types.WebDocumentNoProxy)) and thumb is None:
+    elif isinstance(media, (_tl.WebDocument, _tl.WebDocumentNoProxy)) and thumb is None:
         return await self._download_web_document(
             media, file, progress_callback
         )
@@ -488,15 +487,15 @@ def _get_thumb(thumbs, thumb):
     # last while this is the smallest (layer 116). Ensure we have the
     # sizes sorted correctly with a custom function.
     def sort_thumbs(thumb):
-        if isinstance(thumb, types.PhotoStrippedSize):
+        if isinstance(thumb, _tl.PhotoStrippedSize):
             return 1, len(thumb.bytes)
-        if isinstance(thumb, types.PhotoCachedSize):
+        if isinstance(thumb, _tl.PhotoCachedSize):
             return 1, len(thumb.bytes)
-        if isinstance(thumb, types.PhotoSize):
+        if isinstance(thumb, _tl.PhotoSize):
             return 1, thumb.size
-        if isinstance(thumb, types.PhotoSizeProgressive):
+        if isinstance(thumb, _tl.PhotoSizeProgressive):
             return 1, max(thumb.sizes)
-        if isinstance(thumb, types.VideoSize):
+        if isinstance(thumb, _tl.VideoSize):
             return 2, thumb.size
 
         # Empty size or invalid should go last
@@ -508,7 +507,7 @@ def _get_thumb(thumbs, thumb):
         # :tl:`PhotoPathSize` is used for animated stickers preview, and the thumb is actually
         # a SVG path of the outline. Users expect thumbnails to be JPEG files, so pretend this
         # thumb size doesn't actually exist (#1655).
-        if isinstance(thumbs[i], types.PhotoPathSize):
+        if isinstance(thumbs[i], _tl.PhotoPathSize):
             thumbs.pop(i)
 
     if thumb is None:
@@ -517,15 +516,15 @@ def _get_thumb(thumbs, thumb):
         return thumbs[thumb]
     elif isinstance(thumb, str):
         return next((t for t in thumbs if t.type == thumb), None)
-    elif isinstance(thumb, (types.PhotoSize, types.PhotoCachedSize,
-                            types.PhotoStrippedSize, types.VideoSize)):
+    elif isinstance(thumb, (_tl.PhotoSize, _tl.PhotoCachedSize,
+                            _tl.PhotoStrippedSize, _tl.VideoSize)):
         return thumb
     else:
         return None
 
 def _download_cached_photo_size(self: 'TelegramClient', size, file):
     # No need to download anything, simply write the bytes
-    if isinstance(size, types.PhotoStrippedSize):
+    if isinstance(size, _tl.PhotoStrippedSize):
         data = utils.stripped_photo_to_jpg(size.bytes)
     else:
         data = size.bytes
@@ -548,31 +547,31 @@ def _download_cached_photo_size(self: 'TelegramClient', size, file):
 async def _download_photo(self: 'TelegramClient', photo, file, date, thumb, progress_callback):
     """Specialized version of .download_media() for photos"""
     # Determine the photo and its largest size
-    if isinstance(photo, types.MessageMediaPhoto):
+    if isinstance(photo, _tl.MessageMediaPhoto):
         photo = photo.photo
-    if not isinstance(photo, types.Photo):
+    if not isinstance(photo, _tl.Photo):
         return
 
     # Include video sizes here (but they may be None so provide an empty list)
     size = self._get_thumb(photo.sizes + (photo.video_sizes or []), thumb)
-    if not size or isinstance(size, types.PhotoSizeEmpty):
+    if not size or isinstance(size, _tl.PhotoSizeEmpty):
         return
 
-    if isinstance(size, types.VideoSize):
+    if isinstance(size, _tl.VideoSize):
         file = self._get_proper_filename(file, 'video', '.mp4', date=date)
     else:
         file = self._get_proper_filename(file, 'photo', '.jpg', date=date)
 
-    if isinstance(size, (types.PhotoCachedSize, types.PhotoStrippedSize)):
+    if isinstance(size, (_tl.PhotoCachedSize, _tl.PhotoStrippedSize)):
         return self._download_cached_photo_size(size, file)
 
-    if isinstance(size, types.PhotoSizeProgressive):
+    if isinstance(size, _tl.PhotoSizeProgressive):
         file_size = max(size.sizes)
     else:
         file_size = size.size
 
     result = await self.download_file(
-        types.InputPhotoFileLocation(
+        _tl.InputPhotoFileLocation(
             id=photo.id,
             access_hash=photo.access_hash,
             file_reference=photo.file_reference,
@@ -589,10 +588,10 @@ def _get_kind_and_names(attributes):
     kind = 'document'
     possible_names = []
     for attr in attributes:
-        if isinstance(attr, types.DocumentAttributeFilename):
+        if isinstance(attr, _tl.DocumentAttributeFilename):
             possible_names.insert(0, attr.file_name)
 
-        elif isinstance(attr, types.DocumentAttributeAudio):
+        elif isinstance(attr, _tl.DocumentAttributeAudio):
             kind = 'audio'
             if attr.performer and attr.title:
                 possible_names.append('{} - {}'.format(
@@ -610,9 +609,9 @@ def _get_kind_and_names(attributes):
 async def _download_document(
         self, document, file, date, thumb, progress_callback, msg_data):
     """Specialized version of .download_media() for documents."""
-    if isinstance(document, types.MessageMediaDocument):
+    if isinstance(document, _tl.MessageMediaDocument):
         document = document.document
-    if not isinstance(document, types.Document):
+    if not isinstance(document, _tl.Document):
         return
 
     if thumb is None:
@@ -625,11 +624,11 @@ async def _download_document(
     else:
         file = self._get_proper_filename(file, 'photo', '.jpg', date=date)
         size = self._get_thumb(document.thumbs, thumb)
-        if isinstance(size, (types.PhotoCachedSize, types.PhotoStrippedSize)):
+        if isinstance(size, (_tl.PhotoCachedSize, _tl.PhotoStrippedSize)):
             return self._download_cached_photo_size(size, file)
 
     result = await self._download_file(
-        types.InputDocumentFileLocation(
+        _tl.InputDocumentFileLocation(
             id=document.id,
             access_hash=document.access_hash,
             file_reference=document.file_reference,
diff --git a/telethon/_client/messageparse.py b/telethon/_client/messageparse.py
index 0cbdb40c..d68ab38f 100644
--- a/telethon/_client/messageparse.py
+++ b/telethon/_client/messageparse.py
@@ -2,8 +2,7 @@ import itertools
 import re
 import typing
 
-from .. import helpers, utils
-from ..tl import types
+from .. import helpers, utils, _tl
 
 if typing.TYPE_CHECKING:
     from .telegramclient import TelegramClient
@@ -25,7 +24,7 @@ async def _replace_with_mention(self: 'TelegramClient', entities, i, user):
     or do nothing if it can't be found.
     """
     try:
-        entities[i] = types.InputMessageEntityMentionName(
+        entities[i] = _tl.InputMessageEntityMentionName(
             entities[i].offset, entities[i].length,
             await self.get_input_entity(user)
         )
@@ -52,15 +51,15 @@ async def _parse_message_text(self: 'TelegramClient', message, parse_mode):
 
     for i in reversed(range(len(msg_entities))):
         e = msg_entities[i]
-        if isinstance(e, types.MessageEntityTextUrl):
+        if isinstance(e, _tl.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)):
+        elif isinstance(e, (_tl.MessageEntityMentionName,
+                            _tl.InputMessageEntityMentionName)):
             is_mention = await self._replace_with_mention(msg_entities, i, e.user_id)
             if not is_mention:
                 del msg_entities[i]
@@ -76,10 +75,10 @@ def _get_response_message(self: 'TelegramClient', request, result, input_chat):
 
     If ``request.random_id`` is a list, this method returns a list too.
     """
-    if isinstance(result, types.UpdateShort):
+    if isinstance(result, _tl.UpdateShort):
         updates = [result.update]
         entities = {}
-    elif isinstance(result, (types.Updates, types.UpdatesCombined)):
+    elif isinstance(result, (_tl.Updates, _tl.UpdatesCombined)):
         updates = result.updates
         entities = {utils.get_peer_id(x): x
                     for x in
@@ -90,11 +89,11 @@ def _get_response_message(self: 'TelegramClient', request, result, input_chat):
     random_to_id = {}
     id_to_message = {}
     for update in updates:
-        if isinstance(update, types.UpdateMessageID):
+        if isinstance(update, _tl.UpdateMessageID):
             random_to_id[update.random_id] = update.id
 
         elif isinstance(update, (
-                types.UpdateNewChannelMessage, types.UpdateNewMessage)):
+                _tl.UpdateNewChannelMessage, _tl.UpdateNewMessage)):
             update.message._finish_init(self, entities, input_chat)
 
             # Pinning a message with `updatePinnedMessage` seems to
@@ -109,7 +108,7 @@ def _get_response_message(self: 'TelegramClient', request, result, input_chat):
             else:
                 return update.message
 
-        elif (isinstance(update, types.UpdateEditMessage)
+        elif (isinstance(update, _tl.UpdateEditMessage)
                 and helpers._entity_type(request.peer) != helpers._EntityType.CHANNEL):
             update.message._finish_init(self, entities, input_chat)
 
@@ -120,26 +119,26 @@ def _get_response_message(self: 'TelegramClient', request, result, input_chat):
             elif request.id == update.message.id:
                 return update.message
 
-        elif (isinstance(update, types.UpdateEditChannelMessage)
+        elif (isinstance(update, _tl.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):
+        elif isinstance(update, _tl.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):
+        elif isinstance(update, _tl.UpdateMessagePoll):
             if request.media.poll.id == update.poll_id:
-                m = types.Message(
+                m = _tl.Message(
                     id=request.id,
                     peer_id=utils.get_peer(request.peer),
-                    media=types.MessageMediaPoll(
+                    media=_tl.MessageMediaPoll(
                         poll=update.poll,
                         results=update.results
                     )
diff --git a/telethon/_client/messages.py b/telethon/_client/messages.py
index 1dde08ec..ba88c665 100644
--- a/telethon/_client/messages.py
+++ b/telethon/_client/messages.py
@@ -3,9 +3,8 @@ import itertools
 import typing
 import warnings
 
-from .. import helpers, utils, errors, hints
+from .. import helpers, utils, errors, hints, _tl
 from ..requestiter import RequestIter
-from ..tl import types, functions
 
 _MAX_CHUNK_SIZE = 100
 
@@ -67,31 +66,31 @@ class _MessagesIter(RequestIter):
         # If we want to perform global a search with `from_user` we have to perform
         # a normal `messages.search`, *but* we can make the entity be `inputPeerEmpty`.
         if not self.entity and from_user:
-            self.entity = types.InputPeerEmpty()
+            self.entity = _tl.InputPeerEmpty()
 
         if filter is None:
-            filter = types.InputMessagesFilterEmpty()
+            filter = _tl.InputMessagesFilterEmpty()
         else:
             filter = filter() if isinstance(filter, type) else filter
 
         if not self.entity:
-            self.request = functions.messages.SearchGlobalRequest(
+            self.request = _tl.fn.messages.SearchGlobal(
                 q=search or '',
                 filter=filter,
                 min_date=None,
                 max_date=offset_date,
                 offset_rate=0,
-                offset_peer=types.InputPeerEmpty(),
+                offset_peer=_tl.InputPeerEmpty(),
                 offset_id=offset_id,
                 limit=1
             )
         elif scheduled:
-            self.request = functions.messages.GetScheduledHistoryRequest(
+            self.request = _tl.fn.messages.GetScheduledHistory(
                 peer=entity,
                 hash=0
             )
         elif reply_to is not None:
-            self.request = functions.messages.GetRepliesRequest(
+            self.request = _tl.fn.messages.GetReplies(
                 peer=self.entity,
                 msg_id=reply_to,
                 offset_id=offset_id,
@@ -102,7 +101,7 @@ class _MessagesIter(RequestIter):
                 min_id=0,
                 hash=0
             )
-        elif search is not None or not isinstance(filter, types.InputMessagesFilterEmpty) or from_user:
+        elif search is not None or not isinstance(filter, _tl.InputMessagesFilterEmpty) or from_user:
             # Telegram completely ignores `from_id` in private chats
             ty = helpers._entity_type(self.entity)
             if ty == helpers._EntityType.USER:
@@ -114,7 +113,7 @@ class _MessagesIter(RequestIter):
                 # and set `from_id` to None to avoid checking it locally.
                 self.from_id = None
 
-            self.request = functions.messages.SearchRequest(
+            self.request = _tl.fn.messages.Search(
                 peer=self.entity,
                 q=search or '',
                 filter=filter,
@@ -136,13 +135,13 @@ class _MessagesIter(RequestIter):
             #
             # Even better, using `filter` and `from_id` seems to always
             # trigger `RPC_CALL_FAIL` which is "internal issues"...
-            if not isinstance(filter, types.InputMessagesFilterEmpty) \
+            if not isinstance(filter, _tl.InputMessagesFilterEmpty) \
                     and offset_date and not search and not offset_id:
                 async for m in self.client.iter_messages(
                         self.entity, 1, offset_date=offset_date):
                     self.request.offset_id = m.id + 1
         else:
-            self.request = functions.messages.GetHistoryRequest(
+            self.request = _tl.fn.messages.GetHistory(
                 peer=self.entity,
                 limit=1,
                 offset_date=offset_date,
@@ -156,7 +155,7 @@ class _MessagesIter(RequestIter):
         if self.limit <= 0:
             # No messages, but we still need to know the total message count
             result = await self.client(self.request)
-            if isinstance(result, types.messages.MessagesNotModified):
+            if isinstance(result, _tl.messages.MessagesNotModified):
                 self.total = result.count
             else:
                 self.total = getattr(result, 'count', len(result.messages))
@@ -189,7 +188,7 @@ class _MessagesIter(RequestIter):
 
         messages = reversed(r.messages) if self.reverse else r.messages
         for message in messages:
-            if (isinstance(message, types.MessageEmpty)
+            if (isinstance(message, _tl.MessageEmpty)
                     or self.from_id and message.sender_id != self.from_id):
                 continue
 
@@ -245,7 +244,7 @@ class _MessagesIter(RequestIter):
             # We want to skip the one we already have
             self.request.offset_id += 1
 
-        if isinstance(self.request, functions.messages.SearchRequest):
+        if isinstance(self.request, _tl.fn.messages.SearchRequest):
             # Unlike getHistory and searchGlobal that use *offset* date,
             # this is *max* date. This means that doing a search in reverse
             # will break it. Since it's not really needed once we're going
@@ -255,11 +254,11 @@ class _MessagesIter(RequestIter):
             # getHistory, searchGlobal and getReplies call it offset_date
             self.request.offset_date = last_message.date
 
-        if isinstance(self.request, functions.messages.SearchGlobalRequest):
+        if isinstance(self.request, _tl.fn.messages.SearchGlobalRequest):
             if last_message.input_chat:
                 self.request.offset_peer = last_message.input_chat
             else:
-                self.request.offset_peer = types.InputPeerEmpty()
+                self.request.offset_peer = _tl.InputPeerEmpty()
 
             self.request.offset_rate = getattr(response, 'next_rate', 0)
 
@@ -287,16 +286,16 @@ class _IDsIter(RequestIter):
         if self._ty == helpers._EntityType.CHANNEL:
             try:
                 r = await self.client(
-                    functions.channels.GetMessagesRequest(self._entity, ids))
+                    _tl.fn.channels.GetMessages(self._entity, ids))
             except errors.MessageIdsEmptyError:
                 # All IDs were invalid, use a dummy result
-                r = types.messages.MessagesNotModified(len(ids))
+                r = _tl.messages.MessagesNotModified(len(ids))
         else:
-            r = await self.client(functions.messages.GetMessagesRequest(ids))
+            r = await self.client(_tl.fn.messages.GetMessages(ids))
             if self._entity:
                 from_id = await self.client._get_peer(self._entity)
 
-        if isinstance(r, types.messages.MessagesNotModified):
+        if isinstance(r, _tl.messages.MessagesNotModified):
             self.buffer.extend(None for _ in ids)
             return
 
@@ -312,7 +311,7 @@ class _IDsIter(RequestIter):
         # since the user can enter arbitrary numbers which can belong to
         # arbitrary chats. Validate these unless ``from_id is None``.
         for message in r.messages:
-            if isinstance(message, types.MessageEmpty) or (
+            if isinstance(message, _tl.MessageEmpty) or (
                     from_id and message.peer_id != from_id):
                 self.buffer.append(None)
             else:
@@ -331,7 +330,7 @@ def iter_messages(
         min_id: int = 0,
         add_offset: int = 0,
         search: str = None,
-        filter: 'typing.Union[types.TypeMessagesFilter, typing.Type[types.TypeMessagesFilter]]' = None,
+        filter: 'typing.Union[_tl.TypeMessagesFilter, typing.Type[_tl.TypeMessagesFilter]]' = None,
         from_user: 'hints.EntityLike' = None,
         wait_time: float = None,
         ids: 'typing.Union[int, typing.Sequence[int]]' = None,
@@ -393,9 +392,9 @@ async def get_messages(self: 'TelegramClient', *args, **kwargs) -> 'hints.TotalL
 async def _get_comment_data(
         self: 'TelegramClient',
         entity: 'hints.EntityLike',
-        message: 'typing.Union[int, types.Message]'
+        message: 'typing.Union[int, _tl.Message]'
 ):
-    r = await self(functions.messages.GetDiscussionMessageRequest(
+    r = await self(_tl.fn.messages.GetDiscussionMessage(
         peer=entity,
         msg_id=utils.get_message_id(message)
     ))
@@ -408,10 +407,10 @@ async def send_message(
         entity: 'hints.EntityLike',
         message: 'hints.MessageLike' = '',
         *,
-        reply_to: 'typing.Union[int, types.Message]' = None,
-        attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None,
+        reply_to: 'typing.Union[int, _tl.Message]' = None,
+        attributes: 'typing.Sequence[_tl.TypeDocumentAttribute]' = None,
         parse_mode: typing.Optional[str] = (),
-        formatting_entities: typing.Optional[typing.List[types.TypeMessageEntity]] = None,
+        formatting_entities: typing.Optional[typing.List[_tl.TypeMessageEntity]] = None,
         link_preview: bool = True,
         file: 'typing.Union[hints.FileLike, typing.Sequence[hints.FileLike]]' = None,
         thumb: 'hints.FileLike' = None,
@@ -422,8 +421,8 @@ async def send_message(
         background: bool = None,
         supports_streaming: bool = False,
         schedule: 'hints.DateLike' = None,
-        comment_to: 'typing.Union[int, types.Message]' = None
-) -> 'types.Message':
+        comment_to: 'typing.Union[int, _tl.Message]' = None
+) -> '_tl.Message':
     if file is not None:
         return await self.send_file(
             entity, file, caption=message, reply_to=reply_to,
@@ -439,7 +438,7 @@ async def send_message(
     if comment_to is not None:
         entity, reply_to = await self._get_comment_data(entity, comment_to)
 
-    if isinstance(message, types.Message):
+    if isinstance(message, _tl.Message):
         if buttons is None:
             markup = message.reply_markup
         else:
@@ -449,7 +448,7 @@ async def send_message(
             silent = message.silent
 
         if (message.media and not isinstance(
-                message.media, types.MessageMediaWebPage)):
+                message.media, _tl.MessageMediaWebPage)):
             return await self.send_file(
                 entity,
                 message.media,
@@ -462,7 +461,7 @@ async def send_message(
                 schedule=schedule
             )
 
-        request = functions.messages.SendMessageRequest(
+        request = _tl.fn.messages.SendMessage(
             peer=entity,
             message=message.message or '',
             silent=silent,
@@ -472,7 +471,7 @@ async def send_message(
             entities=message.entities,
             clear_draft=clear_draft,
             no_webpage=not isinstance(
-                message.media, types.MessageMediaWebPage),
+                message.media, _tl.MessageMediaWebPage),
             schedule_date=schedule
         )
         message = message.message
@@ -484,7 +483,7 @@ async def send_message(
                 'The message cannot be empty unless a file is provided'
             )
 
-        request = functions.messages.SendMessageRequest(
+        request = _tl.fn.messages.SendMessage(
             peer=entity,
             message=message,
             entities=formatting_entities,
@@ -498,8 +497,8 @@ async def send_message(
         )
 
     result = await self(request)
-    if isinstance(result, types.UpdateShortSentMessage):
-        message = types.Message(
+    if isinstance(result, _tl.UpdateShortSentMessage):
+        message = _tl.Message(
             id=result.id,
             peer_id=await self._get_peer(entity),
             message=message,
@@ -526,7 +525,7 @@ async def forward_messages(
         silent: bool = None,
         as_album: bool = None,
         schedule: 'hints.DateLike' = None
-) -> 'typing.Sequence[types.Message]':
+) -> 'typing.Sequence[_tl.Message]':
     if as_album is not None:
         warnings.warn('the as_album argument is deprecated and no longer has any effect')
 
@@ -548,7 +547,7 @@ async def forward_messages(
                 return from_peer_id
 
             raise ValueError('from_peer must be given if integer IDs are used')
-        elif isinstance(m, types.Message):
+        elif isinstance(m, _tl.Message):
             return m.chat_id
         else:
             raise TypeError('Cannot forward messages of type {}'.format(type(m)))
@@ -562,7 +561,7 @@ async def forward_messages(
             chat = await chunk[0].get_input_chat()
             chunk = [m.id for m in chunk]
 
-        req = functions.messages.ForwardMessagesRequest(
+        req = _tl.fn.messages.ForwardMessages(
             from_peer=chat,
             id=chunk,
             to_peer=entity,
@@ -578,13 +577,13 @@ async def forward_messages(
 
 async def edit_message(
         self: 'TelegramClient',
-        entity: 'typing.Union[hints.EntityLike, types.Message]',
+        entity: 'typing.Union[hints.EntityLike, _tl.Message]',
         message: 'hints.MessageLike' = None,
         text: str = None,
         *,
         parse_mode: str = (),
-        attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None,
-        formatting_entities: typing.Optional[typing.List[types.TypeMessageEntity]] = None,
+        attributes: 'typing.Sequence[_tl.TypeDocumentAttribute]' = None,
+        formatting_entities: typing.Optional[typing.List[_tl.TypeMessageEntity]] = None,
         link_preview: bool = True,
         file: 'hints.FileLike' = None,
         thumb: 'hints.FileLike' = None,
@@ -592,11 +591,11 @@ async def edit_message(
         buttons: 'hints.MarkupLike' = None,
         supports_streaming: bool = False,
         schedule: 'hints.DateLike' = None
-) -> 'types.Message':
-    if isinstance(entity, types.InputBotInlineMessageID):
+) -> '_tl.Message':
+    if isinstance(entity, _tl.InputBotInlineMessageID):
         text = text or message
         message = entity
-    elif isinstance(entity, types.Message):
+    elif isinstance(entity, _tl.Message):
         text = message  # Shift the parameters to the right
         message = entity
         entity = entity.peer_id
@@ -609,8 +608,8 @@ async def edit_message(
             attributes=attributes,
             force_document=force_document)
 
-    if isinstance(entity, types.InputBotInlineMessageID):
-        request = functions.messages.EditInlineBotMessageRequest(
+    if isinstance(entity, _tl.InputBotInlineMessageID):
+        request = _tl.fn.messages.EditInlineBotMessage(
             id=entity,
             message=text,
             no_webpage=not link_preview,
@@ -631,7 +630,7 @@ async def edit_message(
             return await self(request)
 
     entity = await self.get_input_entity(entity)
-    request = functions.messages.EditMessageRequest(
+    request = _tl.fn.messages.EditMessage(
         peer=entity,
         id=utils.get_message_id(message),
         message=text,
@@ -649,13 +648,13 @@ async def delete_messages(
         entity: 'hints.EntityLike',
         message_ids: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]',
         *,
-        revoke: bool = True) -> 'typing.Sequence[types.messages.AffectedMessages]':
+        revoke: bool = True) -> 'typing.Sequence[_tl.messages.AffectedMessages]':
     if not utils.is_list_like(message_ids):
         message_ids = (message_ids,)
 
     message_ids = (
         m.id if isinstance(m, (
-            types.Message, types.MessageService, types.MessageEmpty))
+            _tl.Message, _tl.MessageService, _tl.MessageEmpty))
         else int(m) for m in message_ids
     )
 
@@ -667,10 +666,10 @@ async def delete_messages(
         ty = helpers._EntityType.USER
 
     if ty == helpers._EntityType.CHANNEL:
-        return await self([functions.channels.DeleteMessagesRequest(
+        return await self([_tl.fn.channels.DeleteMessages(
                         entity, list(c)) for c in utils.chunks(message_ids)])
     else:
-        return await self([functions.messages.DeleteMessagesRequest(
+        return await self([_tl.fn.messages.DeleteMessages(
                         list(c), revoke) for c in utils.chunks(message_ids)])
 
 async def send_read_acknowledge(
@@ -691,16 +690,16 @@ async def send_read_acknowledge(
 
     entity = await self.get_input_entity(entity)
     if clear_mentions:
-        await self(functions.messages.ReadMentionsRequest(entity))
+        await self(_tl.fn.messages.ReadMentions(entity))
         if max_id is None:
             return True
 
     if max_id is not None:
         if helpers._entity_type(entity) == helpers._EntityType.CHANNEL:
-            return await self(functions.channels.ReadHistoryRequest(
+            return await self(_tl.fn.channels.ReadHistory(
                 utils.get_input_channel(entity), max_id=max_id))
         else:
-            return await self(functions.messages.ReadHistoryRequest(
+            return await self(_tl.fn.messages.ReadHistory(
                 entity, max_id=max_id))
 
     return False
@@ -728,10 +727,10 @@ async def _pin(self, entity, message, *, unpin, notify=False, pm_oneside=False):
     message = utils.get_message_id(message) or 0
     entity = await self.get_input_entity(entity)
     if message <= 0:  # old behaviour accepted negative IDs to unpin
-        await self(functions.messages.UnpinAllMessagesRequest(entity))
+        await self(_tl.fn.messages.UnpinAllMessages(entity))
         return
 
-    request = functions.messages.UpdatePinnedMessageRequest(
+    request = _tl.fn.messages.UpdatePinnedMessage(
         peer=entity,
         id=message,
         silent=not notify,
diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py
index 256e8e6f..9c5d62d7 100644
--- a/telethon/_client/telegrambaseclient.py
+++ b/telethon/_client/telegrambaseclient.py
@@ -7,15 +7,13 @@ import platform
 import time
 import typing
 
-from .. import version, helpers, __name__ as __base_name__
+from .. import version, helpers, __name__ as __base_name__, _tl
 from ..crypto import rsa
 from ..entitycache import EntityCache
 from ..extensions import markdown
 from ..network import MTProtoSender, Connection, ConnectionTcpFull, TcpMTProxy
 from ..sessions import Session, SQLiteSession, MemorySession
 from ..statecache import StateCache
-from ..tl import functions, types
-from ..tl.alltlobjects import LAYER
 
 DEFAULT_DC_ID = 2
 DEFAULT_IPV4_IP = '149.154.167.51'
@@ -122,7 +120,7 @@ def init(
             import warnings
             warnings.warn(
                 'The sqlite3 module is not available under this '
-                'Python installation and no custom session '
+                'Python installation and no _ session '
                 'instance was given; using MemorySession.\n'
                 'You will need to re-login every time unless '
                 'you use another session storage'
@@ -198,7 +196,7 @@ def init(
     assert isinstance(connection, type)
     self._connection = connection
     init_proxy = None if not issubclass(connection, TcpMTProxy) else \
-        types.InputClientProxy(*connection.address_info(proxy))
+        _tl.InputClientProxy(*connection.address_info(proxy))
 
     # Used on connection. Capture the variables in a lambda since
     # exporting clients need to create this InvokeWithLayerRequest.
@@ -212,7 +210,7 @@ def init(
         default_device_model = system.machine
     default_system_version = re.sub(r'-.+','',system.release)
 
-    self._init_request = functions.InitConnectionRequest(
+    self._init_request = _tl.fn.InitConnection(
         api_id=self.api_id,
         device_model=device_model or default_device_model or 'Unknown',
         system_version=system_version or default_system_version or '1.0',
@@ -322,10 +320,10 @@ async def connect(self: 'TelegramClient') -> None:
     self.session.auth_key = self._sender.auth_key
     self.session.save()
 
-    self._init_request.query = functions.help.GetConfigRequest()
+    self._init_request.query = _tl.fn.help.GetConfig()
 
-    await self._sender.send(functions.InvokeWithLayerRequest(
-        LAYER, self._init_request
+    await self._sender.send(_tl.fn.InvokeWithLayer(
+        _tl.alltlobjects.LAYER, self._init_request
     ))
 
     self._updates_handle = self.loop.create_task(self._update_loop())
@@ -339,7 +337,7 @@ async def disconnect(self: 'TelegramClient'):
 
 def set_proxy(self: 'TelegramClient', proxy: typing.Union[tuple, dict]):
     init_proxy = None if not issubclass(self._connection, TcpMTProxy) else \
-        types.InputClientProxy(*self._connection.address_info(proxy))
+        _tl.InputClientProxy(*self._connection.address_info(proxy))
 
     self._init_request.proxy = init_proxy
     self._proxy = proxy
@@ -386,7 +384,7 @@ async def _disconnect_coro(self: 'TelegramClient'):
 
     pts, date = self._state_cache[None]
     if pts and date:
-        self.session.set_update_state(0, types.updates.State(
+        self.session.set_update_state(0, _tl.updates.State(
             pts=pts,
             qts=0,
             date=date,
@@ -436,10 +434,10 @@ async def _get_dc(self: 'TelegramClient', dc_id, cdn=False):
     """Gets the Data Center (DC) associated to 'dc_id'"""
     cls = self.__class__
     if not cls._config:
-        cls._config = await self(functions.help.GetConfigRequest())
+        cls._config = await self(_tl.fn.help.GetConfig())
 
     if cdn and not self._cdn_config:
-        cls._cdn_config = await self(functions.help.GetCdnConfigRequest())
+        cls._cdn_config = await self(_tl.fn.help.GetCdnConfig())
         for pk in cls._cdn_config.public_keys:
             rsa.add_key(pk.public_key)
 
@@ -481,9 +479,9 @@ async def _create_exported_sender(self: 'TelegramClient', dc_id):
         local_addr=self._local_addr
     ))
     self._log[__name__].info('Exporting auth for new borrowed sender in %s', dc)
-    auth = await self(functions.auth.ExportAuthorizationRequest(dc_id))
-    self._init_request.query = functions.auth.ImportAuthorizationRequest(id=auth.id, bytes=auth.bytes)
-    req = functions.InvokeWithLayerRequest(LAYER, self._init_request)
+    auth = await self(_tl.fn.auth.ExportAuthorization(dc_id))
+    self._init_request.query = _tl.fn.auth.ImportAuthorization(id=auth.id, bytes=auth.bytes)
+    req = _tl.fn.InvokeWithLayer(LAYER, self._init_request)
     await sender.send(req)
     return sender
 
diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py
index 01cd5d14..d3935273 100644
--- a/telethon/_client/telegramclient.py
+++ b/telethon/_client/telegramclient.py
@@ -8,8 +8,7 @@ from . import (
     account, auth, bots, buttons, chats, dialogs, downloads, messageparse, messages,
     telegrambaseclient, updates, uploads, users
 )
-from .. import helpers, version
-from ..tl import types, custom
+from .. import helpers, version, _tl
 from ..network import ConnectionTcpFull
 from ..events.common import EventBuilder, EventCommon
 
@@ -369,7 +368,7 @@ class TelegramClient:
             *,
             password: str = None,
             bot_token: str = None,
-            phone_code_hash: str = None) -> 'typing.Union[types.User, types.auth.SentCode]':
+            phone_code_hash: str = None) -> 'typing.Union[_tl.User, _tl.auth.SentCode]':
         """
         Logs in to Telegram to an existing user or bot account.
 
@@ -428,7 +427,7 @@ class TelegramClient:
             last_name: str = '',
             *,
             phone: str = None,
-            phone_code_hash: str = None) -> 'types.User':
+            phone_code_hash: str = None) -> '_tl.User':
         """
         Signs up to Telegram as a new user account.
 
@@ -477,7 +476,7 @@ class TelegramClient:
             self: 'TelegramClient',
             phone: str,
             *,
-            force_sms: bool = False) -> 'types.auth.SentCode':
+            force_sms: bool = False) -> '_tl.auth.SentCode':
         """
         Sends the Telegram code needed to login to the given phone number.
 
@@ -630,7 +629,7 @@ class TelegramClient:
             *,
             entity: 'hints.EntityLike' = None,
             offset: str = None,
-            geo_point: 'types.GeoPoint' = None) -> custom.InlineResults:
+            geo_point: '_tl.GeoPoint' = None) -> custom.InlineResults:
         """
         Makes an inline query to the specified bot (``@vote New Poll``).
 
@@ -679,7 +678,7 @@ class TelegramClient:
     @staticmethod
     def build_reply_markup(
             buttons: 'typing.Optional[hints.MarkupLike]',
-            inline_only: bool = False) -> 'typing.Optional[types.TypeReplyMarkup]':
+            inline_only: bool = False) -> 'typing.Optional[_tl.TypeReplyMarkup]':
         """
         Builds a :tl:`ReplyInlineMarkup` or :tl:`ReplyKeyboardMarkup` for
         the given buttons.
@@ -722,7 +721,7 @@ class TelegramClient:
             limit: float = None,
             *,
             search: str = '',
-            filter: 'types.TypeChannelParticipantsFilter' = None,
+            filter: '_tl.TypeChannelParticipantsFilter' = None,
             aggressive: bool = False) -> chats._ParticipantsIter:
         """
         Iterator over the participants belonging to the specified chat.
@@ -1018,7 +1017,7 @@ class TelegramClient:
     def action(
             self: 'TelegramClient',
             entity: 'hints.EntityLike',
-            action: 'typing.Union[str, types.TypeSendMessageAction]',
+            action: 'typing.Union[str, _tl.TypeSendMessageAction]',
             *,
             delay: float = 4,
             auto_cancel: bool = True) -> 'typing.Union[_ChatAction, typing.Coroutine]':
@@ -1109,7 +1108,7 @@ class TelegramClient:
             manage_call: bool = None,
             anonymous: bool = None,
             is_admin: bool = None,
-            title: str = None) -> types.Updates:
+            title: str = None) -> _tl.Updates:
         """
         Edits admin permissions for someone in a chat.
 
@@ -1216,7 +1215,7 @@ class TelegramClient:
             send_polls: bool = True,
             change_info: bool = True,
             invite_users: bool = True,
-            pin_messages: bool = True) -> types.Updates:
+            pin_messages: bool = True) -> _tl.Updates:
         """
         Edits user restrictions in a chat.
 
@@ -1396,7 +1395,7 @@ class TelegramClient:
     async def get_stats(
             self: 'TelegramClient',
             entity: 'hints.EntityLike',
-            message: 'typing.Union[int, types.Message]' = None,
+            message: 'typing.Union[int, _tl.Message]' = None,
     ):
         """
         Retrieves statistics from the given megagroup or broadcast channel.
@@ -1449,7 +1448,7 @@ class TelegramClient:
             *,
             offset_date: 'hints.DateLike' = None,
             offset_id: int = 0,
-            offset_peer: 'hints.EntityLike' = types.InputPeerEmpty(),
+            offset_peer: 'hints.EntityLike' = _tl.InputPeerEmpty(),
             ignore_pinned: bool = False,
             ignore_migrated: bool = False,
             folder: int = None,
@@ -1604,7 +1603,7 @@ class TelegramClient:
             folder: typing.Union[int, typing.Sequence[int]] = None,
             *,
             unpack=None
-    ) -> types.Updates:
+    ) -> _tl.Updates:
         """
         Edits the folder used by one or more dialogs to archive them.
 
@@ -1756,7 +1755,7 @@ class TelegramClient:
             message: 'hints.MessageLike',
             file: 'hints.FileLike' = None,
             *,
-            thumb: 'typing.Union[int, types.TypePhotoSize]' = None,
+            thumb: 'typing.Union[int, _tl.TypePhotoSize]' = None,
             progress_callback: 'hints.ProgressCallback' = None) -> typing.Optional[typing.Union[str, bytes]]:
         """
         Downloads the given media from a message object.
@@ -1850,7 +1849,7 @@ class TelegramClient:
             input_location (:tl:`InputFileLocation`):
                 The file location from which the file will be downloaded.
                 See `telethon.utils.get_input_location` source for a complete
-                list of supported types.
+                list of supported _tl.
 
             file (`str` | `file`, optional):
                 The output file path, directory, or stream-like object.
@@ -2048,7 +2047,7 @@ class TelegramClient:
             min_id: int = 0,
             add_offset: int = 0,
             search: str = None,
-            filter: 'typing.Union[types.TypeMessagesFilter, typing.Type[types.TypeMessagesFilter]]' = None,
+            filter: 'typing.Union[_tl.TypeMessagesFilter, typing.Type[_tl.TypeMessagesFilter]]' = None,
             from_user: 'hints.EntityLike' = None,
             wait_time: float = None,
             ids: 'typing.Union[int, typing.Sequence[int]]' = None,
@@ -2258,10 +2257,10 @@ class TelegramClient:
             entity: 'hints.EntityLike',
             message: 'hints.MessageLike' = '',
             *,
-            reply_to: 'typing.Union[int, types.Message]' = None,
-            attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None,
+            reply_to: 'typing.Union[int, _tl.Message]' = None,
+            attributes: 'typing.Sequence[_tl.TypeDocumentAttribute]' = None,
             parse_mode: typing.Optional[str] = (),
-            formatting_entities: typing.Optional[typing.List[types.TypeMessageEntity]] = None,
+            formatting_entities: typing.Optional[typing.List[_tl.TypeMessageEntity]] = None,
             link_preview: bool = True,
             file: 'typing.Union[hints.FileLike, typing.Sequence[hints.FileLike]]' = None,
             thumb: 'hints.FileLike' = None,
@@ -2272,8 +2271,8 @@ class TelegramClient:
             background: bool = None,
             supports_streaming: bool = False,
             schedule: 'hints.DateLike' = None,
-            comment_to: 'typing.Union[int, types.Message]' = None
-    ) -> 'types.Message':
+            comment_to: 'typing.Union[int, _tl.Message]' = None
+    ) -> '_tl.Message':
         """
         Sends a message to the specified user, chat or channel.
 
@@ -2458,7 +2457,7 @@ class TelegramClient:
             silent: bool = None,
             as_album: bool = None,
             schedule: 'hints.DateLike' = None
-    ) -> 'typing.Sequence[types.Message]':
+    ) -> 'typing.Sequence[_tl.Message]':
         """
         Forwards the given messages to the specified entity.
 
@@ -2531,13 +2530,13 @@ class TelegramClient:
 
     async def edit_message(
             self: 'TelegramClient',
-            entity: 'typing.Union[hints.EntityLike, types.Message]',
+            entity: 'typing.Union[hints.EntityLike, _tl.Message]',
             message: 'hints.MessageLike' = None,
             text: str = None,
             *,
             parse_mode: str = (),
-            attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None,
-            formatting_entities: typing.Optional[typing.List[types.TypeMessageEntity]] = None,
+            attributes: 'typing.Sequence[_tl.TypeDocumentAttribute]' = None,
+            formatting_entities: typing.Optional[typing.List[_tl.TypeMessageEntity]] = None,
             link_preview: bool = True,
             file: 'hints.FileLike' = None,
             thumb: 'hints.FileLike' = None,
@@ -2545,7 +2544,7 @@ class TelegramClient:
             buttons: 'hints.MarkupLike' = None,
             supports_streaming: bool = False,
             schedule: 'hints.DateLike' = None
-    ) -> 'types.Message':
+    ) -> '_tl.Message':
         """
         Edits the given message to change its text or media.
 
@@ -2663,7 +2662,7 @@ class TelegramClient:
             entity: 'hints.EntityLike',
             message_ids: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]',
             *,
-            revoke: bool = True) -> 'typing.Sequence[types.messages.AffectedMessages]':
+            revoke: bool = True) -> 'typing.Sequence[_tl.messages.AffectedMessages]':
         """
         Deletes the given messages, optionally "for everyone".
 
@@ -3171,11 +3170,11 @@ class TelegramClient:
             clear_draft: bool = False,
             progress_callback: 'hints.ProgressCallback' = None,
             reply_to: 'hints.MessageIDLike' = None,
-            attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None,
+            attributes: 'typing.Sequence[_tl.TypeDocumentAttribute]' = None,
             thumb: 'hints.FileLike' = None,
             allow_cache: bool = True,
             parse_mode: str = (),
-            formatting_entities: typing.Optional[typing.List[types.TypeMessageEntity]] = None,
+            formatting_entities: typing.Optional[typing.List[_tl.TypeMessageEntity]] = None,
             voice_note: bool = False,
             video_note: bool = False,
             buttons: 'hints.MarkupLike' = None,
@@ -3183,9 +3182,9 @@ class TelegramClient:
             background: bool = None,
             supports_streaming: bool = False,
             schedule: 'hints.DateLike' = None,
-            comment_to: 'typing.Union[int, types.Message]' = None,
+            comment_to: 'typing.Union[int, _tl.Message]' = None,
             ttl: int = None,
-            **kwargs) -> 'types.Message':
+            **kwargs) -> '_tl.Message':
         """
         Sends message with the given file to the specified entity.
 
@@ -3392,11 +3391,11 @@ class TelegramClient:
 
                 # Dices, including dart and other future emoji
                 from telethon.tl import types
-                await client.send_file(chat, types.InputMediaDice(''))
-                await client.send_file(chat, types.InputMediaDice('🎯'))
+                await client.send_file(chat, _tl.InputMediaDice(''))
+                await client.send_file(chat, _tl.InputMediaDice('🎯'))
 
                 # Contacts
-                await client.send_file(chat, types.InputMediaContact(
+                await client.send_file(chat, _tl.InputMediaContact(
                     phone_number='+34 123 456 789',
                     first_name='Example',
                     last_name='',
@@ -3415,7 +3414,7 @@ class TelegramClient:
             use_cache: type = None,
             key: bytes = None,
             iv: bytes = None,
-            progress_callback: 'hints.ProgressCallback' = None) -> 'types.TypeInputFile':
+            progress_callback: 'hints.ProgressCallback' = None) -> '_tl.TypeInputFile':
         """
         Uploads a file to Telegram's servers, without sending it.
 
@@ -3522,7 +3521,7 @@ class TelegramClient:
         return users.call(self._sender, request, ordered=ordered)
 
     async def get_me(self: 'TelegramClient', input_peer: bool = False) \
-            -> 'typing.Union[types.User, types.InputPeerUser]':
+            -> 'typing.Union[_tl.User, _tl.InputPeerUser]':
         """
         Gets "me", the current :tl:`User` who is logged in.
 
@@ -3633,7 +3632,7 @@ class TelegramClient:
 
     async def get_input_entity(
             self: 'TelegramClient',
-            peer: 'hints.EntityLike') -> 'types.TypeInputPeer':
+            peer: 'hints.EntityLike') -> '_tl.TypeInputPeer':
         """
         Turns the given entity into its input entity version.
 
diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py
index 89816530..e8c5709c 100644
--- a/telethon/_client/updates.py
+++ b/telethon/_client/updates.py
@@ -8,9 +8,8 @@ import traceback
 import typing
 import logging
 
-from .. import events, utils, errors
+from .. import events, utils, errors, _tl
 from ..events.common import EventBuilder, EventCommon
-from ..tl import types, functions
 
 if typing.TYPE_CHECKING:
     from .telegramclient import TelegramClient
@@ -22,7 +21,7 @@ Callback = typing.Callable[[typing.Any], typing.Any]
 async def _run_until_disconnected(self: 'TelegramClient'):
     try:
         # Make a high-level request to notify that we want updates
-        await self(functions.updates.GetStateRequest())
+        await self(_tl.fn.updates.GetState())
         return await self.disconnected
     except KeyboardInterrupt:
         pass
@@ -32,7 +31,7 @@ async def _run_until_disconnected(self: 'TelegramClient'):
 async def set_receive_updates(self: 'TelegramClient', receive_updates):
     self._no_updates = not receive_updates
     if receive_updates:
-        await self(functions.updates.GetStateRequest())
+        await self(_tl.fn.updates.GetState())
 
 async def run_until_disconnected(self: 'TelegramClient'):
     return await self._run_until_disconnected()
@@ -91,24 +90,24 @@ async def catch_up(self: 'TelegramClient'):
     self.session.catching_up = True
     try:
         while True:
-            d = await self(functions.updates.GetDifferenceRequest(
+            d = await self(_tl.fn.updates.GetDifference(
                 pts, date, 0
             ))
-            if isinstance(d, (types.updates.DifferenceSlice,
-                                types.updates.Difference)):
-                if isinstance(d, types.updates.Difference):
+            if isinstance(d, (_tl.updates.DifferenceSlice,
+                                _tl.updates.Difference)):
+                if isinstance(d, _tl.updates.Difference):
                     state = d.state
                 else:
                     state = d.intermediate_state
 
                 pts, date = state.pts, state.date
-                self._handle_update(types.Updates(
+                self._handle_update(_tl.Updates(
                     users=d.users,
                     chats=d.chats,
                     date=state.date,
                     seq=state.seq,
                     updates=d.other_updates + [
-                        types.UpdateNewMessage(m, 0, 0)
+                        _tl.UpdateNewMessage(m, 0, 0)
                         for m in d.new_messages
                     ]
                 ))
@@ -128,9 +127,9 @@ async def catch_up(self: 'TelegramClient'):
                 # some). This can be used to detect collisions (i.e.
                 # it would return an update we have already seen).
             else:
-                if isinstance(d, types.updates.DifferenceEmpty):
+                if isinstance(d, _tl.updates.DifferenceEmpty):
                     date = d.date
-                elif isinstance(d, types.updates.DifferenceTooLong):
+                elif isinstance(d, _tl.updates.DifferenceTooLong):
                     pts = d.pts
                 break
     except (ConnectionError, asyncio.CancelledError):
@@ -148,12 +147,12 @@ def _handle_update(self: 'TelegramClient', update):
     self.session.process_entities(update)
     self._entity_cache.add(update)
 
-    if isinstance(update, (types.Updates, types.UpdatesCombined)):
+    if isinstance(update, (_tl.Updates, _tl.UpdatesCombined)):
         entities = {utils.get_peer_id(x): x for x in
                     itertools.chain(update.users, update.chats)}
         for u in update.updates:
             self._process_update(u, update.updates, entities=entities)
-    elif isinstance(update, types.UpdateShort):
+    elif isinstance(update, _tl.UpdateShort):
         self._process_update(update.update, None)
     else:
         self._process_update(update, None)
@@ -230,7 +229,7 @@ async def _update_loop(self: 'TelegramClient'):
                 continue
 
             try:
-                await self(functions.updates.GetStateRequest())
+                await self(_tl.fn.updates.GetState())
             except (ConnectionError, asyncio.CancelledError):
                 return
 
@@ -359,7 +358,7 @@ async def _get_difference(self: 'TelegramClient', update, channel_id, pts_date):
         assert isinstance(channel_id, int), 'channel_id was {}, not int in {}'.format(type(channel_id), update)
         try:
             # Wrap the ID inside a peer to ensure we get a channel back.
-            where = await self.get_input_entity(types.PeerChannel(channel_id))
+            where = await self.get_input_entity(_tl.PeerChannel(channel_id))
         except ValueError:
             # There's a high chance that this fails, since
             # we are getting the difference to fetch entities.
@@ -367,15 +366,15 @@ async def _get_difference(self: 'TelegramClient', update, channel_id, pts_date):
 
         if not pts_date:
             # First-time, can't get difference. Get pts instead.
-            result = await self(functions.channels.GetFullChannelRequest(
+            result = await self(_tl.fn.channels.GetFullChannel(
                 utils.get_input_channel(where)
             ))
             self._state_cache[channel_id] = result.full_chat.pts
             return
 
-        result = await self(functions.updates.GetChannelDifferenceRequest(
+        result = await self(_tl.fn.updates.GetChannelDifference(
             channel=where,
-            filter=types.ChannelMessagesFilterEmpty(),
+            filter=_tl.ChannelMessagesFilterEmpty(),
             pts=pts_date,  # just pts
             limit=100,
             force=True
@@ -383,20 +382,20 @@ async def _get_difference(self: 'TelegramClient', update, channel_id, pts_date):
     else:
         if not pts_date[0]:
             # First-time, can't get difference. Get pts instead.
-            result = await self(functions.updates.GetStateRequest())
+            result = await self(_tl.fn.updates.GetState())
             self._state_cache[None] = result.pts, result.date
             return
 
-        result = await self(functions.updates.GetDifferenceRequest(
+        result = await self(_tl.fn.updates.GetDifference(
             pts=pts_date[0],
             date=pts_date[1],
             qts=0
         ))
 
-    if isinstance(result, (types.updates.Difference,
-                            types.updates.DifferenceSlice,
-                            types.updates.ChannelDifference,
-                            types.updates.ChannelDifferenceTooLong)):
+    if isinstance(result, (_tl.updates.Difference,
+                            _tl.updates.DifferenceSlice,
+                            _tl.updates.ChannelDifference,
+                            _tl.updates.ChannelDifferenceTooLong)):
         update._entities.update({
             utils.get_peer_id(x): x for x in
             itertools.chain(result.users, result.chats)
diff --git a/telethon/_client/uploads.py b/telethon/_client/uploads.py
index 58f69bad..6c0d8146 100644
--- a/telethon/_client/uploads.py
+++ b/telethon/_client/uploads.py
@@ -9,8 +9,7 @@ from io import BytesIO
 
 from ..crypto import AES
 
-from .. import utils, helpers, hints
-from ..tl import types, functions, custom
+from .. import utils, helpers, hints, _tl
 
 try:
     import PIL
@@ -99,11 +98,11 @@ async def send_file(
         clear_draft: bool = False,
         progress_callback: 'hints.ProgressCallback' = None,
         reply_to: 'hints.MessageIDLike' = None,
-        attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None,
+        attributes: 'typing.Sequence[_tl.TypeDocumentAttribute]' = None,
         thumb: 'hints.FileLike' = None,
         allow_cache: bool = True,
         parse_mode: str = (),
-        formatting_entities: typing.Optional[typing.List[types.TypeMessageEntity]] = None,
+        formatting_entities: typing.Optional[typing.List[_tl.TypeMessageEntity]] = None,
         voice_note: bool = False,
         video_note: bool = False,
         buttons: 'hints.MarkupLike' = None,
@@ -111,9 +110,9 @@ async def send_file(
         background: bool = None,
         supports_streaming: bool = False,
         schedule: 'hints.DateLike' = None,
-        comment_to: 'typing.Union[int, types.Message]' = None,
+        comment_to: 'typing.Union[int, _tl.Message]' = None,
         ttl: int = None,
-        **kwargs) -> 'types.Message':
+        **kwargs) -> '_tl.Message':
     # TODO Properly implement allow_cache to reuse the sha256 of the file
     # i.e. `None` was used
     if not file:
@@ -182,7 +181,7 @@ async def send_file(
         raise TypeError('Cannot use {!r} as file'.format(file))
 
     markup = self.build_reply_markup(buttons)
-    request = functions.messages.SendMediaRequest(
+    request = _tl.fn.messages.SendMedia(
         entity, media, reply_to_msg_id=reply_to, message=caption,
         entities=msg_entities, reply_markup=markup, silent=silent,
         schedule_date=schedule, clear_draft=clear_draft,
@@ -225,14 +224,14 @@ async def _send_album(self: 'TelegramClient', entity, files, caption='',
         fh, fm, _ = await self._file_to_media(
             file, supports_streaming=supports_streaming,
             force_document=force_document, ttl=ttl)
-        if isinstance(fm, (types.InputMediaUploadedPhoto, types.InputMediaPhotoExternal)):
-            r = await self(functions.messages.UploadMediaRequest(
+        if isinstance(fm, (_tl.InputMediaUploadedPhoto, _tl.InputMediaPhotoExternal)):
+            r = await self(_tl.fn.messages.UploadMedia(
                 entity, media=fm
             ))
 
             fm = utils.get_input_media(r.photo)
-        elif isinstance(fm, types.InputMediaUploadedDocument):
-            r = await self(functions.messages.UploadMediaRequest(
+        elif isinstance(fm, _tl.InputMediaUploadedDocument):
+            r = await self(_tl.fn.messages.UploadMedia(
                 entity, media=fm
             ))
 
@@ -243,7 +242,7 @@ async def _send_album(self: 'TelegramClient', entity, files, caption='',
             caption, msg_entities = captions.pop()
         else:
             caption, msg_entities = '', None
-        media.append(types.InputSingleMedia(
+        media.append(_tl.InputSingleMedia(
             fm,
             message=caption,
             entities=msg_entities
@@ -251,7 +250,7 @@ async def _send_album(self: 'TelegramClient', entity, files, caption='',
         ))
 
     # Now we can construct the multi-media request
-    request = functions.messages.SendMultiMediaRequest(
+    request = _tl.fn.messages.SendMultiMedia(
         entity, reply_to_msg_id=reply_to, multi_media=media,
         silent=silent, schedule_date=schedule, clear_draft=clear_draft,
         background=background
@@ -271,8 +270,8 @@ async def upload_file(
         use_cache: type = None,
         key: bytes = None,
         iv: bytes = None,
-        progress_callback: 'hints.ProgressCallback' = None) -> 'types.TypeInputFile':
-    if isinstance(file, (types.InputFile, types.InputFileBig)):
+        progress_callback: 'hints.ProgressCallback' = None) -> '_tl.TypeInputFile':
+    if isinstance(file, (_tl.InputFile, _tl.InputFileBig)):
         return file  # Already uploaded
 
     pos = 0
@@ -343,10 +342,10 @@ async def upload_file(
             # The SavePartRequest is different depending on whether
             # the file is too large or not (over or less than 10MB)
             if is_big:
-                request = functions.upload.SaveBigFilePartRequest(
+                request = _tl.fn.upload.SaveBigFilePart(
                     file_id, part_index, part_count, part)
             else:
-                request = functions.upload.SaveFilePartRequest(
+                request = _tl.fn.upload.SaveFilePart(
                     file_id, part_index, part)
 
             result = await self(request)
@@ -360,9 +359,9 @@ async def upload_file(
                     'Failed to upload file part {}.'.format(part_index))
 
     if is_big:
-        return types.InputFileBig(file_id, part_count, file_name)
+        return _tl.InputFileBig(file_id, part_count, file_name)
     else:
-        return custom.InputSizedFile(
+        return _tl.custom.InputSizedFile(
             file_id, part_count, file_name, md5=hash_md5, size=file_size
         )
 
@@ -385,7 +384,7 @@ async def _file_to_media(
 
     # `aiofiles` do not base `io.IOBase` but do have `read`, so we
     # just check for the read attribute to see if it's file-like.
-    if not isinstance(file, (str, bytes, types.InputFile, types.InputFileBig))\
+    if not isinstance(file, (str, bytes, _tl.InputFile, _tl.InputFileBig))\
             and not hasattr(file, 'read'):
         # The user may pass a Message containing media (or the media,
         # or anything similar) that should be treated as a file. Try
@@ -411,7 +410,7 @@ async def _file_to_media(
     media = None
     file_handle = None
 
-    if isinstance(file, (types.InputFile, types.InputFileBig)):
+    if isinstance(file, (_tl.InputFile, _tl.InputFileBig)):
         file_handle = file
     elif not isinstance(file, str) or os.path.isfile(file):
         file_handle = await self.upload_file(
@@ -421,9 +420,9 @@ async def _file_to_media(
         )
     elif re.match('https?://', file):
         if as_image:
-            media = types.InputMediaPhotoExternal(file, ttl_seconds=ttl)
+            media = _tl.InputMediaPhotoExternal(file, ttl_seconds=ttl)
         else:
-            media = types.InputMediaDocumentExternal(file, ttl_seconds=ttl)
+            media = _tl.InputMediaDocumentExternal(file, ttl_seconds=ttl)
     else:
         bot_file = utils.resolve_bot_file_id(file)
         if bot_file:
@@ -437,7 +436,7 @@ async def _file_to_media(
             'an HTTP URL or a valid bot-API-like file ID'.format(file)
         )
     elif as_image:
-        media = types.InputMediaUploadedPhoto(file_handle, ttl_seconds=ttl)
+        media = _tl.InputMediaUploadedPhoto(file_handle, ttl_seconds=ttl)
     else:
         attributes, mime_type = utils.get_attributes(
             file,
@@ -457,7 +456,7 @@ async def _file_to_media(
                 thumb = str(thumb.absolute())
             thumb = await self.upload_file(thumb, file_size=file_size)
 
-        media = types.InputMediaUploadedDocument(
+        media = _tl.InputMediaUploadedDocument(
             file=file_handle,
             mime_type=mime_type,
             attributes=attributes,
diff --git a/telethon/_client/users.py b/telethon/_client/users.py
index 0d871878..6209619a 100644
--- a/telethon/_client/users.py
+++ b/telethon/_client/users.py
@@ -4,10 +4,9 @@ import itertools
 import time
 import typing
 
-from .. import errors, helpers, utils, hints
+from .. import errors, helpers, utils, hints, _tl
 from ..errors import MultiError, RPCError
 from ..helpers import retry_range
-from ..tl import TLRequest, types, functions
 
 _NOT_A_REQUEST = lambda: TypeError('You can only invoke requests, not types!')
 
@@ -48,7 +47,7 @@ async def call(self: 'TelegramClient', sender, request, ordered=False, flood_sle
                 raise errors.FloodWaitError(request=r, capture=diff)
 
         if self._no_updates:
-            r = functions.InvokeWithoutUpdatesRequest(r)
+            r = _tl.fn.InvokeWithoutUpdates(r)
 
     request_index = 0
     last_error = None
@@ -128,13 +127,13 @@ async def call(self: 'TelegramClient', sender, request, ordered=False, flood_sle
 
 
 async def get_me(self: 'TelegramClient', input_peer: bool = False) \
-        -> 'typing.Union[types.User, types.InputPeerUser]':
+        -> 'typing.Union[_tl.User, _tl.InputPeerUser]':
     if input_peer and self._self_input_peer:
         return self._self_input_peer
 
     try:
         me = (await self(
-            functions.users.GetUsersRequest([types.InputUserSelf()])))[0]
+            _tl.fn.users.GetUsers([_tl.InputUserSelf()])))[0]
 
         self._bot = me.bot
         if not self._self_input_peer:
@@ -165,7 +164,7 @@ async def is_user_authorized(self: 'TelegramClient') -> bool:
     if self._authorized is None:
         try:
             # Any request that requires authorization will work
-            await self(functions.updates.GetStateRequest())
+            await self(_tl.fn.updates.GetState())
             self._authorized = True
         except errors.RPCError:
             self._authorized = False
@@ -209,14 +208,14 @@ async def get_entity(
         tmp = []
         while users:
             curr, users = users[:200], users[200:]
-            tmp.extend(await self(functions.users.GetUsersRequest(curr)))
+            tmp.extend(await self(_tl.fn.users.GetUsers(curr)))
         users = tmp
     if chats:  # TODO Handle chats slice?
         chats = (await self(
-            functions.messages.GetChatsRequest([x.chat_id for x in chats]))).chats
+            _tl.fn.messages.GetChats([x.chat_id for x in chats]))).chats
     if channels:
         channels = (await self(
-            functions.channels.GetChannelsRequest(channels))).chats
+            _tl.fn.channels.GetChannels(channels))).chats
 
     # Merge users, chats and channels into a single dictionary
     id_entity = {
@@ -232,19 +231,19 @@ async def get_entity(
     for x in inputs:
         if isinstance(x, str):
             result.append(await self._get_entity_from_string(x))
-        elif not isinstance(x, types.InputPeerSelf):
+        elif not isinstance(x, _tl.InputPeerSelf):
             result.append(id_entity[utils.get_peer_id(x)])
         else:
             result.append(next(
                 u for u in id_entity.values()
-                if isinstance(u, types.User) and u.is_self
+                if isinstance(u, _tl.User) and u.is_self
             ))
 
     return result[0] if single else result
 
 async def get_input_entity(
         self: 'TelegramClient',
-        peer: 'hints.EntityLike') -> 'types.TypeInputPeer':
+        peer: 'hints.EntityLike') -> '_tl.TypeInputPeer':
     # Short-circuit if the input parameter directly maps to an InputPeer
     try:
         return utils.get_input_peer(peer)
@@ -261,7 +260,7 @@ async def get_input_entity(
 
     # Then come known strings that take precedence
     if peer in ('me', 'self'):
-        return types.InputPeerSelf()
+        return _tl.InputPeerSelf()
 
     # No InputPeer, cached peer, or known string. Fetch from disk cache
     try:
@@ -279,10 +278,10 @@ async def get_input_entity(
     # If we're not a bot but the user is in our contacts, it seems to work
     # regardless. These are the only two special-cased requests.
     peer = utils.get_peer(peer)
-    if isinstance(peer, types.PeerUser):
-        users = await self(functions.users.GetUsersRequest([
-            types.InputUser(peer.user_id, access_hash=0)]))
-        if users and not isinstance(users[0], types.UserEmpty):
+    if isinstance(peer, _tl.PeerUser):
+        users = await self(_tl.fn.users.GetUsers([
+            _tl.InputUser(peer.user_id, access_hash=0)]))
+        if users and not isinstance(users[0], _tl.UserEmpty):
             # If the user passed a valid ID they expect to work for
             # channels but would be valid for users, we get UserEmpty.
             # Avoid returning the invalid empty input peer for that.
@@ -291,12 +290,12 @@ async def get_input_entity(
             # it's not, work as a chat and try to validate it through
             # another request, but that becomes too much work.
             return utils.get_input_peer(users[0])
-    elif isinstance(peer, types.PeerChat):
-        return types.InputPeerChat(peer.chat_id)
-    elif isinstance(peer, types.PeerChannel):
+    elif isinstance(peer, _tl.PeerChat):
+        return _tl.InputPeerChat(peer.chat_id)
+    elif isinstance(peer, _tl.PeerChannel):
         try:
-            channels = await self(functions.channels.GetChannelsRequest([
-                types.InputChannel(peer.channel_id, access_hash=0)]))
+            channels = await self(_tl.fn.channels.GetChannels([
+                _tl.InputChannel(peer.channel_id, access_hash=0)]))
             return utils.get_input_peer(channels.chats[0])
         except errors.ChannelInvalidError:
             pass
@@ -326,7 +325,7 @@ async def get_peer_id(
     except AttributeError:
         peer = await self.get_input_entity(peer)
 
-    if isinstance(peer, types.InputPeerSelf):
+    if isinstance(peer, _tl.InputPeerSelf):
         peer = await self.get_me(input_peer=True)
 
     return utils.get_peer_id(peer, add_mark=add_mark)
@@ -348,7 +347,7 @@ async def _get_entity_from_string(self: 'TelegramClient', string):
     if phone:
         try:
             for user in (await self(
-                    functions.contacts.GetContactsRequest(0))).users:
+                    _tl.fn.contacts.GetContacts(0))).users:
                 if user.phone == phone:
                     return user
         except errors.BotMethodInvalidError:
@@ -360,26 +359,26 @@ async def _get_entity_from_string(self: 'TelegramClient', string):
         username, is_join_chat = utils.parse_username(string)
         if is_join_chat:
             invite = await self(
-                functions.messages.CheckChatInviteRequest(username))
+                _tl.fn.messages.CheckChatInvite(username))
 
-            if isinstance(invite, types.ChatInvite):
+            if isinstance(invite, _tl.ChatInvite):
                 raise ValueError(
                     'Cannot get entity from a channel (or group) '
                     'that you are not part of. Join the group and retry'
                 )
-            elif isinstance(invite, types.ChatInviteAlready):
+            elif isinstance(invite, _tl.ChatInviteAlready):
                 return invite.chat
         elif username:
             try:
                 result = await self(
-                    functions.contacts.ResolveUsernameRequest(username))
+                    _tl.fn.contacts.ResolveUsername(username))
             except errors.UsernameNotOccupiedError as e:
                 raise ValueError('No user has "{}" as username'
                                     .format(username)) from e
 
             try:
                 pid = utils.get_peer_id(result.peer, add_mark=False)
-                if isinstance(result.peer, types.PeerUser):
+                if isinstance(result.peer, _tl.PeerUser):
                     return next(x for x in result.users if x.id == pid)
                 else:
                     return next(x for x in result.chats if x.id == pid)
@@ -407,11 +406,11 @@ async def _get_input_dialog(self: 'TelegramClient', dialog):
             dialog.peer = await self.get_input_entity(dialog.peer)
             return dialog
         elif dialog.SUBCLASS_OF_ID == 0xc91c90b6:  # crc32(b'InputPeer')
-            return types.InputDialogPeer(dialog)
+            return _tl.InputDialogPeer(dialog)
     except AttributeError:
         pass
 
-    return types.InputDialogPeer(await self.get_input_entity(dialog))
+    return _tl.InputDialogPeer(await self.get_input_entity(dialog))
 
 async def _get_input_notify(self: 'TelegramClient', notify):
     """
@@ -421,10 +420,10 @@ async def _get_input_notify(self: 'TelegramClient', notify):
     """
     try:
         if notify.SUBCLASS_OF_ID == 0x58981615:
-            if isinstance(notify, types.InputNotifyPeer):
+            if isinstance(notify, _tl.InputNotifyPeer):
                 notify.peer = await self.get_input_entity(notify.peer)
             return notify
     except AttributeError:
         pass
 
-    return types.InputNotifyPeer(await self.get_input_entity(notify))
+    return _tl.InputNotifyPeer(await self.get_input_entity(notify))
diff --git a/telethon/_crypto/cdndecrypter.py b/telethon/_crypto/cdndecrypter.py
index dd615a5a..efdc3288 100644
--- a/telethon/_crypto/cdndecrypter.py
+++ b/telethon/_crypto/cdndecrypter.py
@@ -3,8 +3,7 @@ This module holds the CdnDecrypter utility class.
 """
 from hashlib import sha256
 
-from ..tl.functions.upload import GetCdnFileRequest, ReuploadCdnFileRequest
-from ..tl.types.upload import CdnFileReuploadNeeded, CdnFile
+from .. import _tl
 from ..crypto import AESModeCTR
 from ..errors import CdnFileTamperedError
 
@@ -52,14 +51,14 @@ class CdnDecrypter:
             cdn_aes, cdn_redirect.cdn_file_hashes
         )
 
-        cdn_file = await cdn_client(GetCdnFileRequest(
+        cdn_file = await cdn_client(_tl.fn.upload.GetCdnFile(
             file_token=cdn_redirect.file_token,
             offset=cdn_redirect.cdn_file_hashes[0].offset,
             limit=cdn_redirect.cdn_file_hashes[0].limit
         ))
-        if isinstance(cdn_file, CdnFileReuploadNeeded):
+        if isinstance(cdn_file, _tl.upload.CdnFileReuploadNeeded):
             # We need to use the original client here
-            await client(ReuploadCdnFileRequest(
+            await client(_tl.fn.upload.ReuploadCdnFile(
                 file_token=cdn_redirect.file_token,
                 request_token=cdn_file.request_token
             ))
@@ -82,13 +81,13 @@ class CdnDecrypter:
         """
         if self.cdn_file_hashes:
             cdn_hash = self.cdn_file_hashes.pop(0)
-            cdn_file = self.client(GetCdnFileRequest(
+            cdn_file = self.client(_tl.fn.upload.GetCdnFile(
                 self.file_token, cdn_hash.offset, cdn_hash.limit
             ))
             cdn_file.bytes = self.cdn_aes.encrypt(cdn_file.bytes)
             self.check(cdn_file.bytes, cdn_hash)
         else:
-            cdn_file = CdnFile(bytes(0))
+            cdn_file = _tl.upload.CdnFile(bytes(0))
 
         return cdn_file
 
diff --git a/telethon/_crypto/rsa.py b/telethon/_crypto/rsa.py
index 91ca7bad..d1f1b588 100644
--- a/telethon/_crypto/rsa.py
+++ b/telethon/_crypto/rsa.py
@@ -11,7 +11,7 @@ except ImportError:
     rsa = None
     raise ImportError('Missing module "rsa", please install via pip.')
 
-from ..tl import TLObject
+from .. import _tl
 
 
 # {fingerprint: (Crypto.PublicKey.RSA._RSAobj, old)} dictionary
@@ -41,8 +41,8 @@ def _compute_fingerprint(key):
     :param key: the Crypto.RSA key.
     :return: its 8-bytes-long fingerprint.
     """
-    n = TLObject.serialize_bytes(get_byte_array(key.n))
-    e = TLObject.serialize_bytes(get_byte_array(key.e))
+    n = _tl.TLObject.serialize_bytes(get_byte_array(key.n))
+    e = _tl.TLObject.serialize_bytes(get_byte_array(key.e))
     # Telegram uses the last 8 bytes as the fingerprint
     return struct.unpack(' tag, this  tag is
@@ -69,9 +62,9 @@ class HTMLToTelegramParser(HTMLParser):
                 except KeyError:
                     pass
             except KeyError:
-                EntityType = MessageEntityCode
+                EntityType = _tl.MessageEntityCode
         elif tag == 'pre':
-            EntityType = MessageEntityPre
+            EntityType = _tl.MessageEntityPre
             args['language'] = ''
         elif tag == 'a':
             try:
@@ -80,12 +73,12 @@ class HTMLToTelegramParser(HTMLParser):
                 return
             if url.startswith('mailto:'):
                 url = url[len('mailto:'):]
-                EntityType = MessageEntityEmail
+                EntityType = _tl.MessageEntityEmail
             else:
                 if self.get_starttag_text() == url:
-                    EntityType = MessageEntityUrl
+                    EntityType = _tl.MessageEntityUrl
                 else:
-                    EntityType = MessageEntityTextUrl
+                    EntityType = _tl.MessageEntityTextUrl
                     args['url'] = url
                     url = None
             self._open_tags_meta.popleft()
@@ -121,10 +114,10 @@ class HTMLToTelegramParser(HTMLParser):
             self.entities.append(entity)
 
 
-def parse(html: str) -> Tuple[str, List[TypeMessageEntity]]:
+def parse(html: str) -> Tuple[str, List[_tl.TypeMessageEntity]]:
     """
     Parses the given HTML message and returns its stripped representation
-    plus a list of the MessageEntity's that were found.
+    plus a list of the _tl.MessageEntity's that were found.
 
     :param html: the message with HTML to be parsed.
     :return: a tuple consisting of (clean message, [message entities]).
@@ -138,14 +131,14 @@ def parse(html: str) -> Tuple[str, List[TypeMessageEntity]]:
     return _del_surrogate(text), parser.entities
 
 
-def unparse(text: str, entities: Iterable[TypeMessageEntity], _offset: int = 0,
+def unparse(text: str, entities: Iterable[_tl.TypeMessageEntity], _offset: int = 0,
             _length: Optional[int] = None) -> str:
     """
     Performs the reverse operation to .parse(), effectively returning HTML
-    given a normal text and its MessageEntity's.
+    given a normal text and its _tl.MessageEntity's.
 
     :param text: the text to be reconverted into HTML.
-    :param entities: the MessageEntity's applied to the text.
+    :param entities: the _tl.MessageEntity's applied to the text.
     :return: a HTML representation of the combination of both inputs.
     """
     if not text:
@@ -185,19 +178,19 @@ def unparse(text: str, entities: Iterable[TypeMessageEntity], _offset: int = 0,
                               _offset=entity.offset, _length=length)
         entity_type = type(entity)
 
-        if entity_type == MessageEntityBold:
+        if entity_type == _tl.MessageEntityBold:
             html.append('{}'.format(entity_text))
-        elif entity_type == MessageEntityItalic:
+        elif entity_type == _tl.MessageEntityItalic:
             html.append('{}'.format(entity_text))
-        elif entity_type == MessageEntityCode:
+        elif entity_type == _tl.MessageEntityCode:
             html.append('{}'.format(entity_text))
-        elif entity_type == MessageEntityUnderline:
+        elif entity_type == _tl.MessageEntityUnderline:
             html.append('{}'.format(entity_text))
-        elif entity_type == MessageEntityStrike:
+        elif entity_type == _tl.MessageEntityStrike:
             html.append('{}'.format(entity_text))
-        elif entity_type == MessageEntityBlockquote:
+        elif entity_type == _tl.MessageEntityBlockquote:
             html.append('
{}
'.format(entity_text)) - elif entity_type == MessageEntityPre: + elif entity_type == _tl.MessageEntityPre: if entity.language: html.append( "
\n"
@@ -208,14 +201,14 @@ def unparse(text: str, entities: Iterable[TypeMessageEntity], _offset: int = 0,
             else:
                 html.append('
{}
' .format(entity_text)) - elif entity_type == MessageEntityEmail: + elif entity_type == _tl.MessageEntityEmail: html.append('{0}'.format(entity_text)) - elif entity_type == MessageEntityUrl: + elif entity_type == _tl.MessageEntityUrl: html.append('{0}'.format(entity_text)) - elif entity_type == MessageEntityTextUrl: + elif entity_type == _tl.MessageEntityTextUrl: html.append('{}' .format(escape(entity.url), entity_text)) - elif entity_type == MessageEntityMentionName: + elif entity_type == _tl.MessageEntityMentionName: html.append('{}' .format(entity.user_id, entity_text)) else: diff --git a/telethon/_misc/markdown.py b/telethon/_misc/markdown.py index f6d59106..336da0b9 100644 --- a/telethon/_misc/markdown.py +++ b/telethon/_misc/markdown.py @@ -7,19 +7,14 @@ import re import warnings from ..helpers import add_surrogate, del_surrogate, within_surrogate, strip_text -from ..tl import TLObject -from ..tl.types import ( - MessageEntityBold, MessageEntityItalic, MessageEntityCode, - MessageEntityPre, MessageEntityTextUrl, MessageEntityMentionName, - MessageEntityStrike -) +from .. import _tl DEFAULT_DELIMITERS = { - '**': MessageEntityBold, - '__': MessageEntityItalic, - '~~': MessageEntityStrike, - '`': MessageEntityCode, - '```': MessageEntityPre + '**': _tl.MessageEntityBold, + '__': _tl.MessageEntityItalic, + '~~': _tl.MessageEntityStrike, + '`': _tl.MessageEntityCode, + '```': _tl.MessageEntityPre } DEFAULT_URL_RE = re.compile(r'\[([\S\s]+?)\]\((.+?)\)') @@ -33,7 +28,7 @@ def overlap(a, b, x, y): def parse(message, delimiters=None, url_re=None): """ Parses the given markdown message and returns its stripped representation - plus a list of the MessageEntity's that were found. + plus a list of the _tl.MessageEntity's that were found. :param message: the message with markdown-like syntax to be parsed. :param delimiters: the delimiters to be used, {delimiter: type}. @@ -98,13 +93,13 @@ def parse(message, delimiters=None, url_re=None): # Append the found entity ent = delimiters[delim] - if ent == MessageEntityPre: + if ent == _tl.MessageEntityPre: result.append(ent(i, end - i - len(delim), '')) # has 'lang' else: result.append(ent(i, end - i - len(delim))) # No nested entities inside code blocks - if ent in (MessageEntityCode, MessageEntityPre): + if ent in (_tl.MessageEntityCode, _tl.MessageEntityPre): i = end - len(delim) continue @@ -125,7 +120,7 @@ def parse(message, delimiters=None, url_re=None): if ent.offset + ent.length > m.start(): ent.length -= delim_size - result.append(MessageEntityTextUrl( + result.append(_tl.MessageEntityTextUrl( offset=m.start(), length=len(m.group(1)), url=del_surrogate(m.group(2)) )) @@ -141,10 +136,10 @@ def parse(message, delimiters=None, url_re=None): def unparse(text, entities, delimiters=None, url_fmt=None): """ Performs the reverse operation to .parse(), effectively returning - markdown-like syntax given a normal text and its MessageEntity's. + markdown-like syntax given a normal text and its _tl.MessageEntity's. :param text: the text to be reconverted into markdown. - :param entities: the MessageEntity's applied to the text. + :param entities: the _tl.MessageEntity's applied to the text. :return: a markdown-like text representing the combination of both inputs. """ if not text or not entities: @@ -158,7 +153,7 @@ def unparse(text, entities, delimiters=None, url_fmt=None): if url_fmt is not None: warnings.warn('url_fmt is deprecated') # since it complicates everything *a lot* - if isinstance(entities, TLObject): + if isinstance(entities, _tl.TLObject): entities = (entities,) text = add_surrogate(text) @@ -173,9 +168,9 @@ def unparse(text, entities, delimiters=None, url_fmt=None): insert_at.append((e, delimiter)) else: url = None - if isinstance(entity, MessageEntityTextUrl): + if isinstance(entity, _tl.MessageEntityTextUrl): url = entity.url - elif isinstance(entity, MessageEntityMentionName): + elif isinstance(entity, _tl.MessageEntityMentionName): url = 'tg://user?id={}'.format(entity.user_id) if url: insert_at.append((s, '[')) diff --git a/telethon/_misc/password.py b/telethon/_misc/password.py index 0f950254..e02c8eb8 100644 --- a/telethon/_misc/password.py +++ b/telethon/_misc/password.py @@ -2,7 +2,7 @@ import hashlib import os from .crypto import factorization -from .tl import types +from . import _tl def check_prime_and_good_check(prime: int, g: int): @@ -110,7 +110,7 @@ def pbkdf2sha512(password: bytes, salt: bytes, iterations: int): return hashlib.pbkdf2_hmac('sha512', password, salt, iterations) -def compute_hash(algo: types.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow, +def compute_hash(algo: _tl.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow, password: str): hash1 = sha256(algo.salt1, password.encode('utf-8'), algo.salt1) hash2 = sha256(algo.salt2, hash1, algo.salt2) @@ -118,7 +118,7 @@ def compute_hash(algo: types.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter1000 return sha256(algo.salt2, hash3, algo.salt2) -def compute_digest(algo: types.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow, +def compute_digest(algo: _tl.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow, password: str): try: check_prime_and_good(algo.p, algo.g) @@ -133,9 +133,9 @@ def compute_digest(algo: types.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter10 # https://github.com/telegramdesktop/tdesktop/blob/18b74b90451a7db2379a9d753c9cbaf8734b4d5d/Telegram/SourceFiles/core/core_cloud_password.cpp -def compute_check(request: types.account.Password, password: str): +def compute_check(request: _tl.account.Password, password: str): algo = request.current_algo - if not isinstance(algo, types.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow): + if not isinstance(algo, _tl.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow): raise ValueError('unsupported password algorithm {}' .format(algo.__class__.__name__)) @@ -190,5 +190,5 @@ def compute_check(request: types.account.Password, password: str): K ) - return types.InputCheckPasswordSRP( + return _tl.InputCheckPasswordSRP( request.srp_id, bytes(a_for_hash), bytes(M1)) diff --git a/telethon/_misc/statecache.py b/telethon/_misc/statecache.py index 0e02bbd4..3f2475bf 100644 --- a/telethon/_misc/statecache.py +++ b/telethon/_misc/statecache.py @@ -1,6 +1,6 @@ import inspect -from .tl import types +from . import _tl # Which updates have the following fields? @@ -9,8 +9,8 @@ _has_channel_id = [] # TODO EntityCache does the same. Reuse? def _fill(): - for name in dir(types): - update = getattr(types, name) + for name in dir(_tl): + update = getattr(_tl, name) if getattr(update, 'SUBCLASS_OF_ID', None) == 0x9f89304e: cid = update.CONSTRUCTOR_ID sig = inspect.signature(update.__init__) @@ -51,41 +51,41 @@ class StateCache: *, channel_id=None, has_pts=frozenset(x.CONSTRUCTOR_ID for x in ( - types.UpdateNewMessage, - types.UpdateDeleteMessages, - types.UpdateReadHistoryInbox, - types.UpdateReadHistoryOutbox, - types.UpdateWebPage, - types.UpdateReadMessagesContents, - types.UpdateEditMessage, - types.updates.State, - types.updates.DifferenceTooLong, - types.UpdateShortMessage, - types.UpdateShortChatMessage, - types.UpdateShortSentMessage + _tl.UpdateNewMessage, + _tl.UpdateDeleteMessages, + _tl.UpdateReadHistoryInbox, + _tl.UpdateReadHistoryOutbox, + _tl.UpdateWebPage, + _tl.UpdateReadMessagesContents, + _tl.UpdateEditMessage, + _tl.updates.State, + _tl.updates.DifferenceTooLong, + _tl.UpdateShortMessage, + _tl.UpdateShortChatMessage, + _tl.UpdateShortSentMessage )), has_date=frozenset(x.CONSTRUCTOR_ID for x in ( - types.UpdateUserPhoto, - types.UpdateEncryption, - types.UpdateEncryptedMessagesRead, - types.UpdateChatParticipantAdd, - types.updates.DifferenceEmpty, - types.UpdateShortMessage, - types.UpdateShortChatMessage, - types.UpdateShort, - types.UpdatesCombined, - types.Updates, - types.UpdateShortSentMessage, + _tl.UpdateUserPhoto, + _tl.UpdateEncryption, + _tl.UpdateEncryptedMessagesRead, + _tl.UpdateChatParticipantAdd, + _tl.updates.DifferenceEmpty, + _tl.UpdateShortMessage, + _tl.UpdateShortChatMessage, + _tl.UpdateShort, + _tl.UpdatesCombined, + _tl.Updates, + _tl.UpdateShortSentMessage, )), has_channel_pts=frozenset(x.CONSTRUCTOR_ID for x in ( - types.UpdateChannelTooLong, - types.UpdateNewChannelMessage, - types.UpdateDeleteChannelMessages, - types.UpdateEditChannelMessage, - types.UpdateChannelWebPage, - types.updates.ChannelDifferenceEmpty, - types.updates.ChannelDifferenceTooLong, - types.updates.ChannelDifference + _tl.UpdateChannelTooLong, + _tl.UpdateNewChannelMessage, + _tl.UpdateDeleteChannelMessages, + _tl.UpdateEditChannelMessage, + _tl.UpdateChannelWebPage, + _tl.updates.ChannelDifferenceEmpty, + _tl.updates.ChannelDifferenceTooLong, + _tl.updates.ChannelDifference )), check_only=False ): @@ -120,8 +120,8 @@ class StateCache: has_channel_id=frozenset(_has_channel_id), # Hardcoded because only some with message are for channels has_message=frozenset(x.CONSTRUCTOR_ID for x in ( - types.UpdateNewChannelMessage, - types.UpdateEditChannelMessage + _tl.UpdateNewChannelMessage, + _tl.UpdateEditChannelMessage )) ): """ diff --git a/telethon/_misc/utils.py b/telethon/_misc/utils.py index e8c59c01..9826e551 100644 --- a/telethon/_misc/utils.py +++ b/telethon/_misc/utils.py @@ -21,7 +21,7 @@ from types import GeneratorType from .extensions import markdown, html from .helpers import add_surrogate, del_surrogate, strip_text -from .tl import types +from . import _tl try: import hachoir @@ -32,26 +32,26 @@ except ImportError: # Register some of the most common mime-types to avoid any issues. # See https://github.com/LonamiWebs/Telethon/issues/1096. -mimetypes.add_type('image/png', '.png') -mimetypes.add_type('image/jpeg', '.jpeg') -mimetypes.add_type('image/webp', '.webp') -mimetypes.add_type('image/gif', '.gif') -mimetypes.add_type('image/bmp', '.bmp') -mimetypes.add_type('image/x-tga', '.tga') -mimetypes.add_type('image/tiff', '.tiff') -mimetypes.add_type('image/vnd.adobe.photoshop', '.psd') +mime_tl.add_type('image/png', '.png') +mime_tl.add_type('image/jpeg', '.jpeg') +mime_tl.add_type('image/webp', '.webp') +mime_tl.add_type('image/gif', '.gif') +mime_tl.add_type('image/bmp', '.bmp') +mime_tl.add_type('image/x-tga', '.tga') +mime_tl.add_type('image/tiff', '.tiff') +mime_tl.add_type('image/vnd.adobe.photoshop', '.psd') -mimetypes.add_type('video/mp4', '.mp4') -mimetypes.add_type('video/quicktime', '.mov') -mimetypes.add_type('video/avi', '.avi') +mime_tl.add_type('video/mp4', '.mp4') +mime_tl.add_type('video/quicktime', '.mov') +mime_tl.add_type('video/avi', '.avi') -mimetypes.add_type('audio/mpeg', '.mp3') -mimetypes.add_type('audio/m4a', '.m4a') -mimetypes.add_type('audio/aac', '.aac') -mimetypes.add_type('audio/ogg', '.ogg') -mimetypes.add_type('audio/flac', '.flac') +mime_tl.add_type('audio/mpeg', '.mp3') +mime_tl.add_type('audio/m4a', '.m4a') +mime_tl.add_type('audio/aac', '.aac') +mime_tl.add_type('audio/ogg', '.ogg') +mime_tl.add_type('audio/flac', '.flac') -mimetypes.add_type('application/x-tgsticker', '.tgs') +mime_tl.add_type('application/x-tgsticker', '.tgs') USERNAME_RE = re.compile( r'@|(?:https?://)?(?:www\.)?(?:telegram\.(?:me|dog)|t\.me)/(@|joinchat/)?' @@ -92,7 +92,7 @@ def get_display_name(entity): Gets the display name for the given :tl:`User`, :tl:`Chat` or :tl:`Channel`. Returns an empty string otherwise. """ - if isinstance(entity, types.User): + if isinstance(entity, _tl.User): if entity.last_name and entity.first_name: return '{} {}'.format(entity.first_name, entity.last_name) elif entity.first_name: @@ -102,7 +102,7 @@ def get_display_name(entity): else: return '' - elif isinstance(entity, (types.Chat, types.ChatForbidden, types.Channel)): + elif isinstance(entity, (_tl.Chat, _tl.ChatForbidden, _tl.Channel)): return entity.title return '' @@ -117,14 +117,14 @@ def get_extension(media): return '.jpg' except TypeError: # These cases are not handled by input photo because it can't - if isinstance(media, (types.UserProfilePhoto, types.ChatPhoto)): + if isinstance(media, (_tl.UserProfilePhoto, _tl.ChatPhoto)): return '.jpg' # Documents will come with a mime type - if isinstance(media, types.MessageMediaDocument): + if isinstance(media, _tl.MessageMediaDocument): media = media.document if isinstance(media, ( - types.Document, types.WebDocument, types.WebDocumentNoProxy)): + _tl.Document, _tl.WebDocument, _tl.WebDocumentNoProxy)): if media.mime_type == 'application/octet-stream': # Octet stream are just bytes, which have no default extension return '' @@ -184,53 +184,53 @@ def get_input_peer(entity, allow_self=True, check_hash=True): else: _raise_cast_fail(entity, 'InputPeer') - if isinstance(entity, types.User): + if isinstance(entity, _tl.User): if entity.is_self and allow_self: - return types.InputPeerSelf() + return _tl.InputPeerSelf() elif (entity.access_hash is not None and not entity.min) or not check_hash: - return types.InputPeerUser(entity.id, entity.access_hash) + return _tl.InputPeerUser(entity.id, entity.access_hash) else: raise TypeError('User without access_hash or min info cannot be input') - if isinstance(entity, (types.Chat, types.ChatEmpty, types.ChatForbidden)): - return types.InputPeerChat(entity.id) + if isinstance(entity, (_tl.Chat, _tl.ChatEmpty, _tl.ChatForbidden)): + return _tl.InputPeerChat(entity.id) - if isinstance(entity, types.Channel): + if isinstance(entity, _tl.Channel): if (entity.access_hash is not None and not entity.min) or not check_hash: - return types.InputPeerChannel(entity.id, entity.access_hash) + return _tl.InputPeerChannel(entity.id, entity.access_hash) else: raise TypeError('Channel without access_hash or min info cannot be input') - if isinstance(entity, types.ChannelForbidden): + if isinstance(entity, _tl.ChannelForbidden): # "channelForbidden are never min", and since their hash is # also not optional, we assume that this truly is the case. - return types.InputPeerChannel(entity.id, entity.access_hash) + return _tl.InputPeerChannel(entity.id, entity.access_hash) - if isinstance(entity, types.InputUser): - return types.InputPeerUser(entity.user_id, entity.access_hash) + if isinstance(entity, _tl.InputUser): + return _tl.InputPeerUser(entity.user_id, entity.access_hash) - if isinstance(entity, types.InputChannel): - return types.InputPeerChannel(entity.channel_id, entity.access_hash) + if isinstance(entity, _tl.InputChannel): + return _tl.InputPeerChannel(entity.channel_id, entity.access_hash) - if isinstance(entity, types.InputUserSelf): - return types.InputPeerSelf() + if isinstance(entity, _tl.InputUserSelf): + return _tl.InputPeerSelf() - if isinstance(entity, types.InputUserFromMessage): - return types.InputPeerUserFromMessage(entity.peer, entity.msg_id, entity.user_id) + if isinstance(entity, _tl.InputUserFromMessage): + return _tl.InputPeerUserFromMessage(entity.peer, entity.msg_id, entity.user_id) - if isinstance(entity, types.InputChannelFromMessage): - return types.InputPeerChannelFromMessage(entity.peer, entity.msg_id, entity.channel_id) + if isinstance(entity, _tl.InputChannelFromMessage): + return _tl.InputPeerChannelFromMessage(entity.peer, entity.msg_id, entity.channel_id) - if isinstance(entity, types.UserEmpty): - return types.InputPeerEmpty() + if isinstance(entity, _tl.UserEmpty): + return _tl.InputPeerEmpty() - if isinstance(entity, types.UserFull): + if isinstance(entity, _tl.UserFull): return get_input_peer(entity.user) - if isinstance(entity, types.ChatFull): - return types.InputPeerChat(entity.id) + if isinstance(entity, _tl.ChatFull): + return _tl.InputPeerChat(entity.id) - if isinstance(entity, types.PeerChat): - return types.InputPeerChat(entity.chat_id) + if isinstance(entity, _tl.PeerChat): + return _tl.InputPeerChat(entity.chat_id) _raise_cast_fail(entity, 'InputPeer') @@ -251,14 +251,14 @@ def get_input_channel(entity): except AttributeError: _raise_cast_fail(entity, 'InputChannel') - if isinstance(entity, (types.Channel, types.ChannelForbidden)): - return types.InputChannel(entity.id, entity.access_hash or 0) + if isinstance(entity, (_tl.Channel, _tl.ChannelForbidden)): + return _tl.InputChannel(entity.id, entity.access_hash or 0) - if isinstance(entity, types.InputPeerChannel): - return types.InputChannel(entity.channel_id, entity.access_hash) + if isinstance(entity, _tl.InputPeerChannel): + return _tl.InputChannel(entity.channel_id, entity.access_hash) - if isinstance(entity, types.InputPeerChannelFromMessage): - return types.InputChannelFromMessage(entity.peer, entity.msg_id, entity.channel_id) + if isinstance(entity, _tl.InputPeerChannelFromMessage): + return _tl.InputChannelFromMessage(entity.peer, entity.msg_id, entity.channel_id) _raise_cast_fail(entity, 'InputChannel') @@ -279,26 +279,26 @@ def get_input_user(entity): except AttributeError: _raise_cast_fail(entity, 'InputUser') - if isinstance(entity, types.User): + if isinstance(entity, _tl.User): if entity.is_self: - return types.InputUserSelf() + return _tl.InputUserSelf() else: - return types.InputUser(entity.id, entity.access_hash or 0) + return _tl.InputUser(entity.id, entity.access_hash or 0) - if isinstance(entity, types.InputPeerSelf): - return types.InputUserSelf() + if isinstance(entity, _tl.InputPeerSelf): + return _tl.InputUserSelf() - if isinstance(entity, (types.UserEmpty, types.InputPeerEmpty)): - return types.InputUserEmpty() + if isinstance(entity, (_tl.UserEmpty, _tl.InputPeerEmpty)): + return _tl.InputUserEmpty() - if isinstance(entity, types.UserFull): + if isinstance(entity, _tl.UserFull): return get_input_user(entity.user) - if isinstance(entity, types.InputPeerUser): - return types.InputUser(entity.user_id, entity.access_hash) + if isinstance(entity, _tl.InputPeerUser): + return _tl.InputUser(entity.user_id, entity.access_hash) - if isinstance(entity, types.InputPeerUserFromMessage): - return types.InputUserFromMessage(entity.peer, entity.msg_id, entity.user_id) + if isinstance(entity, _tl.InputPeerUserFromMessage): + return _tl.InputUserFromMessage(entity.peer, entity.msg_id, entity.user_id) _raise_cast_fail(entity, 'InputUser') @@ -309,12 +309,12 @@ def get_input_dialog(dialog): if dialog.SUBCLASS_OF_ID == 0xa21c9795: # crc32(b'InputDialogPeer') return dialog if dialog.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer') - return types.InputDialogPeer(dialog) + return _tl.InputDialogPeer(dialog) except AttributeError: _raise_cast_fail(dialog, 'InputDialogPeer') try: - return types.InputDialogPeer(get_input_peer(dialog)) + return _tl.InputDialogPeer(get_input_peer(dialog)) except TypeError: pass @@ -329,18 +329,18 @@ def get_input_document(document): except AttributeError: _raise_cast_fail(document, 'InputDocument') - if isinstance(document, types.Document): - return types.InputDocument( + if isinstance(document, _tl.Document): + return _tl.InputDocument( id=document.id, access_hash=document.access_hash, file_reference=document.file_reference) - if isinstance(document, types.DocumentEmpty): - return types.InputDocumentEmpty() + if isinstance(document, _tl.DocumentEmpty): + return _tl.InputDocumentEmpty() - if isinstance(document, types.MessageMediaDocument): + if isinstance(document, _tl.MessageMediaDocument): return get_input_document(document.document) - if isinstance(document, types.Message): + if isinstance(document, _tl.Message): return get_input_document(document.media) _raise_cast_fail(document, 'InputDocument') @@ -354,32 +354,32 @@ def get_input_photo(photo): except AttributeError: _raise_cast_fail(photo, 'InputPhoto') - if isinstance(photo, types.Message): + if isinstance(photo, _tl.Message): photo = photo.media - if isinstance(photo, (types.photos.Photo, types.MessageMediaPhoto)): + if isinstance(photo, (_tl.photos.Photo, _tl.MessageMediaPhoto)): photo = photo.photo - if isinstance(photo, types.Photo): - return types.InputPhoto(id=photo.id, access_hash=photo.access_hash, + if isinstance(photo, _tl.Photo): + return _tl.InputPhoto(id=photo.id, access_hash=photo.access_hash, file_reference=photo.file_reference) - if isinstance(photo, types.PhotoEmpty): - return types.InputPhotoEmpty() + if isinstance(photo, _tl.PhotoEmpty): + return _tl.InputPhotoEmpty() - if isinstance(photo, types.messages.ChatFull): + if isinstance(photo, _tl.messages.ChatFull): photo = photo.full_chat - if isinstance(photo, types.ChannelFull): + if isinstance(photo, _tl.ChannelFull): return get_input_photo(photo.chat_photo) - elif isinstance(photo, types.UserFull): + elif isinstance(photo, _tl.UserFull): return get_input_photo(photo.profile_photo) - elif isinstance(photo, (types.Channel, types.Chat, types.User)): + elif isinstance(photo, (_tl.Channel, _tl.Chat, _tl.User)): return get_input_photo(photo.photo) - if isinstance(photo, (types.UserEmpty, types.ChatEmpty, - types.ChatForbidden, types.ChannelForbidden)): - return types.InputPhotoEmpty() + if isinstance(photo, (_tl.UserEmpty, _tl.ChatEmpty, + _tl.ChatForbidden, _tl.ChannelForbidden)): + return _tl.InputPhotoEmpty() _raise_cast_fail(photo, 'InputPhoto') @@ -390,15 +390,15 @@ def get_input_chat_photo(photo): if photo.SUBCLASS_OF_ID == 0xd4eb2d74: # crc32(b'InputChatPhoto') return photo elif photo.SUBCLASS_OF_ID == 0xe7655f1f: # crc32(b'InputFile'): - return types.InputChatUploadedPhoto(photo) + return _tl.InputChatUploadedPhoto(photo) except AttributeError: _raise_cast_fail(photo, 'InputChatPhoto') photo = get_input_photo(photo) - if isinstance(photo, types.InputPhoto): - return types.InputChatPhoto(photo) - elif isinstance(photo, types.InputPhotoEmpty): - return types.InputChatPhotoEmpty() + if isinstance(photo, _tl.InputPhoto): + return _tl.InputChatPhoto(photo) + elif isinstance(photo, _tl.InputPhotoEmpty): + return _tl.InputChatPhotoEmpty() _raise_cast_fail(photo, 'InputChatPhoto') @@ -411,16 +411,16 @@ def get_input_geo(geo): except AttributeError: _raise_cast_fail(geo, 'InputGeoPoint') - if isinstance(geo, types.GeoPoint): - return types.InputGeoPoint(lat=geo.lat, long=geo.long) + if isinstance(geo, _tl.GeoPoint): + return _tl.InputGeoPoint(lat=geo.lat, long=geo.long) - if isinstance(geo, types.GeoPointEmpty): - return types.InputGeoPointEmpty() + if isinstance(geo, _tl.GeoPointEmpty): + return _tl.InputGeoPointEmpty() - if isinstance(geo, types.MessageMediaGeo): + if isinstance(geo, _tl.MessageMediaGeo): return get_input_geo(geo.geo) - if isinstance(geo, types.Message): + if isinstance(geo, _tl.Message): return get_input_geo(geo.media) _raise_cast_fail(geo, 'InputGeoPoint') @@ -443,39 +443,39 @@ def get_input_media( if media.SUBCLASS_OF_ID == 0xfaf846f4: # crc32(b'InputMedia') return media elif media.SUBCLASS_OF_ID == 0x846363e0: # crc32(b'InputPhoto') - return types.InputMediaPhoto(media, ttl_seconds=ttl) + return _tl.InputMediaPhoto(media, ttl_seconds=ttl) elif media.SUBCLASS_OF_ID == 0xf33fdb68: # crc32(b'InputDocument') - return types.InputMediaDocument(media, ttl_seconds=ttl) + return _tl.InputMediaDocument(media, ttl_seconds=ttl) except AttributeError: _raise_cast_fail(media, 'InputMedia') - if isinstance(media, types.MessageMediaPhoto): - return types.InputMediaPhoto( + if isinstance(media, _tl.MessageMediaPhoto): + return _tl.InputMediaPhoto( id=get_input_photo(media.photo), ttl_seconds=ttl or media.ttl_seconds ) - if isinstance(media, (types.Photo, types.photos.Photo, types.PhotoEmpty)): - return types.InputMediaPhoto( + if isinstance(media, (_tl.Photo, _tl.photos.Photo, _tl.PhotoEmpty)): + return _tl.InputMediaPhoto( id=get_input_photo(media), ttl_seconds=ttl ) - if isinstance(media, types.MessageMediaDocument): - return types.InputMediaDocument( + if isinstance(media, _tl.MessageMediaDocument): + return _tl.InputMediaDocument( id=get_input_document(media.document), ttl_seconds=ttl or media.ttl_seconds ) - if isinstance(media, (types.Document, types.DocumentEmpty)): - return types.InputMediaDocument( + if isinstance(media, (_tl.Document, _tl.DocumentEmpty)): + return _tl.InputMediaDocument( id=get_input_document(media), ttl_seconds=ttl ) - if isinstance(media, (types.InputFile, types.InputFileBig)): + if isinstance(media, (_tl.InputFile, _tl.InputFileBig)): if is_photo: - return types.InputMediaUploadedPhoto(file=media, ttl_seconds=ttl) + return _tl.InputMediaUploadedPhoto(file=media, ttl_seconds=ttl) else: attrs, mime = get_attributes( media, @@ -485,29 +485,29 @@ def get_input_media( video_note=video_note, supports_streaming=supports_streaming ) - return types.InputMediaUploadedDocument( + return _tl.InputMediaUploadedDocument( file=media, mime_type=mime, attributes=attrs, force_file=force_document, ttl_seconds=ttl) - if isinstance(media, types.MessageMediaGame): - return types.InputMediaGame(id=types.InputGameID( + if isinstance(media, _tl.MessageMediaGame): + return _tl.InputMediaGame(id=_tl.InputGameID( id=media.game.id, access_hash=media.game.access_hash )) - if isinstance(media, types.MessageMediaContact): - return types.InputMediaContact( + if isinstance(media, _tl.MessageMediaContact): + return _tl.InputMediaContact( phone_number=media.phone_number, first_name=media.first_name, last_name=media.last_name, vcard='' ) - if isinstance(media, types.MessageMediaGeo): - return types.InputMediaGeoPoint(geo_point=get_input_geo(media.geo)) + if isinstance(media, _tl.MessageMediaGeo): + return _tl.InputMediaGeoPoint(geo_point=get_input_geo(media.geo)) - if isinstance(media, types.MessageMediaVenue): - return types.InputMediaVenue( + if isinstance(media, _tl.MessageMediaVenue): + return _tl.InputMediaVenue( geo_point=get_input_geo(media.geo), title=media.title, address=media.address, @@ -516,19 +516,19 @@ def get_input_media( venue_type='' ) - if isinstance(media, types.MessageMediaDice): - return types.InputMediaDice(media.emoticon) + if isinstance(media, _tl.MessageMediaDice): + return _tl.InputMediaDice(media.emoticon) if isinstance(media, ( - types.MessageMediaEmpty, types.MessageMediaUnsupported, - types.ChatPhotoEmpty, types.UserProfilePhotoEmpty, - types.ChatPhoto, types.UserProfilePhoto)): - return types.InputMediaEmpty() + _tl.MessageMediaEmpty, _tl.MessageMediaUnsupported, + _tl.ChatPhotoEmpty, _tl.UserProfilePhotoEmpty, + _tl.ChatPhoto, _tl.UserProfilePhoto)): + return _tl.InputMediaEmpty() - if isinstance(media, types.Message): + if isinstance(media, _tl.Message): return get_input_media(media.media, is_photo=is_photo, ttl=ttl) - if isinstance(media, types.MessageMediaPoll): + if isinstance(media, _tl.MessageMediaPoll): if media.poll.quiz: if not media.results.results: # A quiz has correct answers, which we don't know until answered. @@ -539,15 +539,15 @@ def get_input_media( else: correct_answers = None - return types.InputMediaPoll( + return _tl.InputMediaPoll( poll=media.poll, correct_answers=correct_answers, solution=media.results.solution, solution_entities=media.results.solution_entities, ) - if isinstance(media, types.Poll): - return types.InputMediaPoll(media) + if isinstance(media, _tl.Poll): + return _tl.InputMediaPoll(media) _raise_cast_fail(media, 'InputMedia') @@ -556,11 +556,11 @@ 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 types.InputMessageID(message) + return _tl.InputMessageID(message) elif message.SUBCLASS_OF_ID == 0x54b6bcc5: # crc32(b'InputMessage'): return message elif message.SUBCLASS_OF_ID == 0x790009e3: # crc32(b'Message'): - return types.InputMessageID(message.id) + return _tl.InputMessageID(message.id) except AttributeError: pass @@ -573,7 +573,7 @@ def get_input_group_call(call): if call.SUBCLASS_OF_ID == 0x58611ab1: # crc32(b'InputGroupCall') return call elif call.SUBCLASS_OF_ID == 0x20b4f320: # crc32(b'GroupCall') - return types.InputGroupCall(id=call.id, access_hash=call.access_hash) + return _tl.InputGroupCall(id=call.id, access_hash=call.access_hash) except AttributeError: _raise_cast_fail(call, 'InputGroupCall') @@ -675,10 +675,10 @@ def get_attributes(file, *, attributes=None, mime_type=None, # Note: ``file.name`` works for :tl:`InputFile` and some `IOBase` streams name = file if isinstance(file, str) else getattr(file, 'name', 'unnamed') if mime_type is None: - mime_type = mimetypes.guess_type(name)[0] + mime_type = mime_tl.guess_type(name)[0] - attr_dict = {types.DocumentAttributeFilename: - types.DocumentAttributeFilename(os.path.basename(name))} + attr_dict = {_tl.DocumentAttributeFilename: + _tl.DocumentAttributeFilename(os.path.basename(name))} if is_audio(file): m = _get_metadata(file) @@ -690,8 +690,8 @@ def get_attributes(file, *, attributes=None, mime_type=None, else: performer = None - attr_dict[types.DocumentAttributeAudio] = \ - types.DocumentAttributeAudio( + attr_dict[_tl.DocumentAttributeAudio] = \ + _tl.DocumentAttributeAudio( voice=voice_note, title=m.get('title') if m.has('title') else None, performer=performer, @@ -702,7 +702,7 @@ def get_attributes(file, *, attributes=None, mime_type=None, if not force_document and is_video(file): m = _get_metadata(file) if m: - doc = types.DocumentAttributeVideo( + doc = _tl.DocumentAttributeVideo( round_message=video_note, w=m.get('width') if m.has('width') else 1, h=m.get('height') if m.has('height') else 1, @@ -719,22 +719,22 @@ def get_attributes(file, *, attributes=None, mime_type=None, if t_m and t_m.has("height"): height = t_m.get("height") - doc = types.DocumentAttributeVideo( + doc = _tl.DocumentAttributeVideo( 0, width, height, round_message=video_note, supports_streaming=supports_streaming) else: - doc = types.DocumentAttributeVideo( + doc = _tl.DocumentAttributeVideo( 0, 1, 1, round_message=video_note, supports_streaming=supports_streaming) - attr_dict[types.DocumentAttributeVideo] = doc + attr_dict[_tl.DocumentAttributeVideo] = doc if voice_note: - if types.DocumentAttributeAudio in attr_dict: - attr_dict[types.DocumentAttributeAudio].voice = True + if _tl.DocumentAttributeAudio in attr_dict: + attr_dict[_tl.DocumentAttributeAudio].voice = True else: - attr_dict[types.DocumentAttributeAudio] = \ - types.DocumentAttributeAudio(0, voice=True) + attr_dict[_tl.DocumentAttributeAudio] = \ + _tl.DocumentAttributeAudio(0, voice=True) # Now override the attributes if any. As we have a dict of # {cls: instance}, we can override any class with the list @@ -803,23 +803,23 @@ def _get_file_info(location): except AttributeError: _raise_cast_fail(location, 'InputFileLocation') - if isinstance(location, types.Message): + if isinstance(location, _tl.Message): location = location.media - if isinstance(location, types.MessageMediaDocument): + if isinstance(location, _tl.MessageMediaDocument): location = location.document - elif isinstance(location, types.MessageMediaPhoto): + elif isinstance(location, _tl.MessageMediaPhoto): location = location.photo - if isinstance(location, types.Document): - return _FileInfo(location.dc_id, types.InputDocumentFileLocation( + if isinstance(location, _tl.Document): + return _FileInfo(location.dc_id, _tl.InputDocumentFileLocation( id=location.id, access_hash=location.access_hash, file_reference=location.file_reference, thumb_size='' # Presumably to download one of its thumbnails ), location.size) - elif isinstance(location, types.Photo): - return _FileInfo(location.dc_id, types.InputPhotoFileLocation( + elif isinstance(location, _tl.Photo): + return _FileInfo(location.dc_id, _tl.InputPhotoFileLocation( id=location.id, access_hash=location.access_hash, file_reference=location.file_reference, @@ -860,7 +860,7 @@ def is_image(file): if match: return True else: - return isinstance(resolve_bot_file_id(file), types.Photo) + return isinstance(resolve_bot_file_id(file), _tl.Photo) def is_gif(file): @@ -881,7 +881,7 @@ def is_audio(file): return False else: file = 'a' + ext - return (mimetypes.guess_type(file)[0] or '').startswith('audio/') + return (mime_tl.guess_type(file)[0] or '').startswith('audio/') def is_video(file): @@ -895,7 +895,7 @@ def is_video(file): return False else: file = 'a' + ext - return (mimetypes.guess_type(file)[0] or '').startswith('video/') + return (mime_tl.guess_type(file)[0] or '').startswith('video/') def is_list_like(obj): @@ -971,27 +971,27 @@ def get_peer(peer): elif peer.SUBCLASS_OF_ID == 0x2d45687: return peer elif isinstance(peer, ( - types.contacts.ResolvedPeer, types.InputNotifyPeer, - types.TopPeer, types.Dialog, types.DialogPeer)): + _tl.contacts.ResolvedPeer, _tl.InputNotifyPeer, + _tl.TopPeer, _tl.Dialog, _tl.DialogPeer)): return peer.peer - elif isinstance(peer, types.ChannelFull): - return types.PeerChannel(peer.id) - elif isinstance(peer, types.UserEmpty): - return types.PeerUser(peer.id) - elif isinstance(peer, types.ChatEmpty): - return types.PeerChat(peer.id) + elif isinstance(peer, _tl.ChannelFull): + return _tl.PeerChannel(peer.id) + elif isinstance(peer, _tl.UserEmpty): + return _tl.PeerUser(peer.id) + elif isinstance(peer, _tl.ChatEmpty): + return _tl.PeerChat(peer.id) if peer.SUBCLASS_OF_ID in (0x7d7c6f86, 0xd9c7fc18): # ChatParticipant, ChannelParticipant - return types.PeerUser(peer.user_id) + return _tl.PeerUser(peer.user_id) peer = get_input_peer(peer, allow_self=False, check_hash=False) - if isinstance(peer, (types.InputPeerUser, types.InputPeerUserFromMessage)): - return types.PeerUser(peer.user_id) - elif isinstance(peer, types.InputPeerChat): - return types.PeerChat(peer.chat_id) - elif isinstance(peer, (types.InputPeerChannel, types.InputPeerChannelFromMessage)): - return types.PeerChannel(peer.channel_id) + if isinstance(peer, (_tl.InputPeerUser, _tl.InputPeerUserFromMessage)): + return _tl.PeerUser(peer.user_id) + elif isinstance(peer, _tl.InputPeerChat): + return _tl.PeerChat(peer.chat_id) + elif isinstance(peer, (_tl.InputPeerChannel, _tl.InputPeerChannelFromMessage)): + return _tl.PeerChannel(peer.channel_id) except (AttributeError, TypeError): pass _raise_cast_fail(peer, 'Peer') @@ -1017,7 +1017,7 @@ def get_peer_id(peer, add_mark=True): return peer if add_mark else resolve_id(peer)[0] # Tell the user to use their client to resolve InputPeerSelf if we got one - if isinstance(peer, types.InputPeerSelf): + if isinstance(peer, _tl.InputPeerSelf): _raise_cast_fail(peer, 'int (you might want to use client.get_peer_id)') try: @@ -1025,15 +1025,15 @@ def get_peer_id(peer, add_mark=True): except TypeError: _raise_cast_fail(peer, 'int') - if isinstance(peer, types.PeerUser): + if isinstance(peer, _tl.PeerUser): return peer.user_id - elif isinstance(peer, types.PeerChat): + elif isinstance(peer, _tl.PeerChat): # Check in case the user mixed things up to avoid blowing up if not (0 < peer.chat_id <= 0x7fffffff): peer.chat_id = resolve_id(peer.chat_id)[0] return -peer.chat_id if add_mark else peer.chat_id - else: # if isinstance(peer, types.PeerChannel): + else: # if isinstance(peer, _tl.PeerChannel): # Check in case the user mixed things up to avoid blowing up if not (0 < peer.channel_id <= 0x7fffffff): peer.channel_id = resolve_id(peer.channel_id)[0] @@ -1048,14 +1048,14 @@ def get_peer_id(peer, add_mark=True): def resolve_id(marked_id): """Given a marked ID, returns the original ID and its :tl:`Peer` type.""" if marked_id >= 0: - return marked_id, types.PeerUser + return marked_id, _tl.PeerUser marked_id = -marked_id if marked_id > 1000000000000: marked_id -= 1000000000000 - return marked_id, types.PeerChannel + return marked_id, _tl.PeerChannel else: - return marked_id, types.PeerChat + return marked_id, _tl.PeerChat def _rle_decode(data): @@ -1159,12 +1159,12 @@ def resolve_bot_file_id(file_id): attributes = [] if file_type == 3 or file_type == 9: - attributes.append(types.DocumentAttributeAudio( + attributes.append(_tl.DocumentAttributeAudio( duration=0, voice=file_type == 3 )) elif file_type == 4 or file_type == 13: - attributes.append(types.DocumentAttributeVideo( + attributes.append(_tl.DocumentAttributeVideo( duration=0, w=0, h=0, @@ -1172,14 +1172,14 @@ def resolve_bot_file_id(file_id): )) # elif file_type == 5: # other, cannot know which elif file_type == 8: - attributes.append(types.DocumentAttributeSticker( + attributes.append(_tl.DocumentAttributeSticker( alt='', - stickerset=types.InputStickerSetEmpty() + stickerset=_tl.InputStickerSetEmpty() )) elif file_type == 10: - attributes.append(types.DocumentAttributeAnimated()) + attributes.append(_tl.DocumentAttributeAnimated()) - return types.Document( + return _tl.Document( id=media_id, access_hash=access_hash, date=None, @@ -1210,12 +1210,12 @@ def resolve_bot_file_id(file_id): # Thumbnails (small) always have ID 0; otherwise size 'x' photo_size = 's' if media_id or access_hash else 'x' - return types.Photo( + return _tl.Photo( id=media_id, access_hash=access_hash, file_reference=b'', date=None, - sizes=[types.PhotoSize( + sizes=[_tl.PhotoSize( type=photo_size, w=0, h=0, @@ -1235,21 +1235,21 @@ def pack_bot_file_id(file): If an invalid parameter is given, it will ``return None``. """ - if isinstance(file, types.MessageMediaDocument): + if isinstance(file, _tl.MessageMediaDocument): file = file.document - elif isinstance(file, types.MessageMediaPhoto): + elif isinstance(file, _tl.MessageMediaPhoto): file = file.photo - if isinstance(file, types.Document): + if isinstance(file, _tl.Document): file_type = 5 for attribute in file.attributes: - if isinstance(attribute, types.DocumentAttributeAudio): + if isinstance(attribute, _tl.DocumentAttributeAudio): file_type = 3 if attribute.voice else 9 - elif isinstance(attribute, types.DocumentAttributeVideo): + elif isinstance(attribute, _tl.DocumentAttributeVideo): file_type = 13 if attribute.round_message else 4 - elif isinstance(attribute, types.DocumentAttributeSticker): + elif isinstance(attribute, _tl.DocumentAttributeSticker): file_type = 8 - elif isinstance(attribute, types.DocumentAttributeAnimated): + elif isinstance(attribute, _tl.DocumentAttributeAnimated): file_type = 10 else: continue @@ -1258,9 +1258,9 @@ def pack_bot_file_id(file): return _encode_telegram_base64(_rle_encode(struct.pack( ' 1: return super().filter(event) - class Event(EventCommon, SenderGetter): + class Event(EventCommon, _tl.custom.sendergetter.SenderGetter): """ Represents the event of a new album. @@ -150,7 +148,7 @@ class Album(EventBuilder): """ def __init__(self, messages): message = messages[0] - if not message.out and isinstance(message.peer_id, types.PeerUser): + if not message.out and isinstance(message.peer_id, _tl.PeerUser): # Incoming message (e.g. from a bot) has peer_id=us, and # from_id=bot (the actual "chat" from a user's perspective). chat_peer = message.from_id @@ -160,7 +158,7 @@ class Album(EventBuilder): super().__init__(chat_peer=chat_peer, msg_id=message.id, broadcast=bool(message.post)) - SenderGetter.__init__(self, message.sender_id) + _tl.custom.sendergetter.SenderGetter.__init__(self, message.sender_id) self.messages = messages def _set_client(self, client): diff --git a/telethon/events/callbackquery.py b/telethon/events/callbackquery.py index 94e03b7b..954ccf2d 100644 --- a/telethon/events/callbackquery.py +++ b/telethon/events/callbackquery.py @@ -2,9 +2,7 @@ import re import struct from .common import EventBuilder, EventCommon, name_inner_event -from .. import utils -from ..tl import types, functions -from ..tl.custom.sendergetter import SenderGetter +from .. import utils, _tl @name_inner_event @@ -88,13 +86,13 @@ class CallbackQuery(EventBuilder): @classmethod def build(cls, update, others=None, self_id=None): - if isinstance(update, types.UpdateBotCallbackQuery): + if isinstance(update, _tl.UpdateBotCallbackQuery): return cls.Event(update, update.peer, update.msg_id) - elif isinstance(update, types.UpdateInlineBotCallbackQuery): + elif isinstance(update, _tl.UpdateInlineBotCallbackQuery): # See https://github.com/LonamiWebs/Telethon/pull/1005 # The long message ID is actually just msg_id + peer_id mid, pid = struct.unpack(' Date: Sun, 12 Sep 2021 12:35:48 +0200 Subject: [PATCH 041/256] Adapt generator to new subpackage path --- setup.py | 4 ++-- telethon_generator/generators/tlobject.py | 25 ++++++++++++++--------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/setup.py b/setup.py index 2cb63901..c82d236d 100755 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ FRIENDLY_IN = GENERATOR_DIR / 'data/friendly.csv' TLOBJECT_IN_TLS = [Path(x) for x in GENERATOR_DIR.glob('data/*.tl')] TLOBJECT_OUT = LIBRARY_DIR / '_tl' -IMPORT_DEPTH = 2 +TLOBJECT_MOD = 'telethon._tl' DOCS_IN_RES = GENERATOR_DIR / 'data/html' DOCS_OUT = Path('docs') @@ -94,7 +94,7 @@ def generate(which, action='gen'): if clean: clean_tlobjects(TLOBJECT_OUT) else: - generate_tlobjects(tlobjects, layer, IMPORT_DEPTH, TLOBJECT_OUT) + generate_tlobjects(tlobjects, layer, TLOBJECT_MOD, TLOBJECT_OUT) if 'errors' in which: which.remove('errors') diff --git a/telethon_generator/generators/tlobject.py b/telethon_generator/generators/tlobject.py index 0cc02c88..cc37cb92 100644 --- a/telethon_generator/generators/tlobject.py +++ b/telethon_generator/generators/tlobject.py @@ -52,7 +52,7 @@ BASE_TYPES = ('string', 'bytes', 'int', 'long', 'int128', def _write_modules( - out_dir, depth, kind, namespace_tlobjects, type_constructors): + out_dir, in_mod, kind, namespace_tlobjects, type_constructors): # namespace_tlobjects: {'namespace', [TLObject]} out_dir.mkdir(parents=True, exist_ok=True) for ns, tlobjects in namespace_tlobjects.items(): @@ -60,10 +60,11 @@ def _write_modules( with file.open('w') as f, SourceBuilder(f) as builder: builder.writeln(AUTO_GEN_NOTICE) - builder.writeln('from {}.tl.tlobject import TLObject', '.' * depth) - if kind != 'TLObject': - builder.writeln( - 'from {}.tl.tlobject import {}', '.' * depth, kind) + if kind == 'TLObject': + builder.writeln('from .tlobject import TLObject, TLRequest') + builder.writeln('from . import fn') + else: + builder.writeln('from .. import TLObject, TLRequest') builder.writeln('from typing import Optional, List, ' 'Union, TYPE_CHECKING') @@ -124,7 +125,11 @@ def _write_modules( if not name or name in primitives: continue - import_space = '{}.tl.types'.format('.' * depth) + if kind == 'TLObject': + import_space = '.' + else: + import_space = '..' + if '.' in name: namespace = name.split('.')[0] name = name.split('.')[1] @@ -681,7 +686,7 @@ def _write_all_tlobjects(tlobjects, layer, builder): builder.writeln('}') -def generate_tlobjects(tlobjects, layer, import_depth, output_dir): +def generate_tlobjects(tlobjects, layer, input_mod, output_dir): # Group everything by {namespace: [tlobjects]} to generate __init__.py namespace_functions = defaultdict(list) namespace_types = defaultdict(list) @@ -695,10 +700,10 @@ def generate_tlobjects(tlobjects, layer, import_depth, output_dir): namespace_types[tlobject.namespace].append(tlobject) type_constructors[tlobject.result].append(tlobject) - _write_modules(output_dir / 'fn', import_depth, 'TLRequest', - namespace_functions, type_constructors) - _write_modules(output_dir, import_depth - 1, 'TLObject', + _write_modules(output_dir, input_mod, 'TLObject', namespace_types, type_constructors) + _write_modules(output_dir / 'fn', input_mod + '.fn', 'TLRequest', + namespace_functions, type_constructors) filename = output_dir / 'alltlobjects.py' with filename.open('w') as file: From f222dc167e5e9a737341524cf9ff1f493e761932 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 12 Sep 2021 13:27:13 +0200 Subject: [PATCH 042/256] Fix imports --- .gitignore | 2 +- readthedocs/misc/v2-migration-guide.rst | 2 + telethon/__init__.py | 10 +- telethon/_client/auth.py | 4 +- telethon/_client/bots.py | 5 +- telethon/_client/buttons.py | 10 +- telethon/_client/chats.py | 19 +-- telethon/_client/dialogs.py | 13 +- telethon/_client/downloads.py | 9 +- telethon/_client/messages.py | 8 +- telethon/_client/telegrambaseclient.py | 12 +- telethon/_client/telegramclient.py | 5 +- telethon/_client/uploads.py | 8 +- telethon/_client/users.py | 6 +- telethon/_crypto/authkey.py | 2 +- telethon/_crypto/cdndecrypter.py | 2 +- telethon/_misc/entitycache.py | 3 +- telethon/_misc/hints.py | 4 +- telethon/_misc/markdown.py | 2 +- telethon/_misc/messagepacker.py | 6 +- telethon/_misc/password.py | 4 +- telethon/_misc/statecache.py | 2 +- telethon/_misc/utils.py | 44 +++---- telethon/_network/authenticator.py | 4 +- telethon/_network/connection/tcpmtproxy.py | 2 +- telethon/_network/connection/tcpobfuscated.py | 2 +- telethon/_network/mtprotoplainsender.py | 2 +- telethon/_network/mtprotosender.py | 61 ++++----- telethon/_network/mtprotostate.py | 14 +-- telethon/_tl/core/rpcresult.py | 3 +- telethon/_tl/custom/adminlogevent.py | 4 +- telethon/_tl/custom/button.py | 4 +- telethon/_tl/custom/chatgetter.py | 11 +- telethon/_tl/custom/dialog.py | 18 +-- telethon/_tl/custom/draft.py | 4 +- telethon/_tl/custom/file.py | 36 +++--- telethon/_tl/custom/forward.py | 3 +- telethon/_tl/custom/inlinebuilder.py | 40 +++--- telethon/_tl/custom/inlineresult.py | 4 +- telethon/_tl/custom/inputsizedfile.py | 4 +- telethon/_tl/custom/message.py | 116 +++++++++--------- telethon/_tl/custom/messagebutton.py | 28 ++--- telethon/_tl/custom/participantpermissions.py | 6 +- telethon/_tl/custom/qrlogin.py | 3 +- telethon/_tl/patched/__init__.py | 2 +- telethon/events/album.py | 8 +- telethon/events/callbackquery.py | 8 +- telethon/events/chataction.py | 3 +- telethon/events/common.py | 3 +- telethon/events/inlinequery.py | 8 +- telethon/events/messageread.py | 3 +- telethon/events/newmessage.py | 3 +- telethon/events/raw.py | 2 +- telethon/events/userupdate.py | 8 +- telethon/sessions/memory.py | 3 +- telethon/sessions/sqlite.py | 5 +- telethon/sessions/string.py | 2 +- telethon_generator/generators/docs.py | 2 +- telethon_generator/generators/tlobject.py | 2 +- telethon_generator/parsers/errors.py | 2 +- .../parsers/tlobject/tlobject.py | 2 +- tests/telethon/tl/test_serialization.py | 6 +- 62 files changed, 322 insertions(+), 301 deletions(-) diff --git a/.gitignore b/.gitignore index 65fabceb..1e497d62 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,4 @@ __pycache__/ /docs/ # File used to manually test new changes, contains sensitive data -/example.py +/example*.py diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index de1af171..8f9bab4d 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -49,6 +49,8 @@ The following modules have been moved inside ``_misc``: * ``statecache.py`` * ``utils.py`` +// TODO review telethon/__init__.py isn't exposing more than it should + The TelegramClient is no longer made out of mixins -------------------------------------------------- diff --git a/telethon/__init__.py b/telethon/__init__.py index 335abab6..fa01de5b 100644 --- a/telethon/__init__.py +++ b/telethon/__init__.py @@ -1,6 +1,12 @@ +# Note: the import order matters +from ._misc import helpers # no dependencies +from . import _tl # no dependencies +from ._misc import utils # depends on helpers and _tl +from ._tl import custom # depends on utils +from ._misc import hints # depends on custom + from ._client.telegramclient import TelegramClient -from .network import connection -from ._tl import custom +from ._network import connection from ._tl.custom import Button from . import version, events, utils, errors diff --git a/telethon/_client/auth.py b/telethon/_client/auth.py index 859a4e87..ccfaa68b 100644 --- a/telethon/_client/auth.py +++ b/telethon/_client/auth.py @@ -5,7 +5,9 @@ import sys import typing import warnings -from .. import utils, helpers, errors, password as pwd_mod, _tl +from .._misc import utils, helpers, password as pwd_mod +from .. import errors, _tl +from .._tl import custom if typing.TYPE_CHECKING: from .telegramclient import TelegramClient diff --git a/telethon/_client/bots.py b/telethon/_client/bots.py index 0e967ed9..8c2d50fe 100644 --- a/telethon/_client/bots.py +++ b/telethon/_client/bots.py @@ -1,6 +1,7 @@ import typing from .. import hints, _tl +from .._tl import custom if typing.TYPE_CHECKING: from .telegramclient import TelegramClient @@ -13,7 +14,7 @@ async def inline_query( *, entity: 'hints.EntityLike' = None, offset: str = None, - geo_point: '_tl.GeoPoint' = None) -> _tl.custom.InlineResults: + geo_point: '_tl.GeoPoint' = None) -> custom.InlineResults: bot = await self.get_input_entity(bot) if entity: peer = await self.get_input_entity(entity) @@ -28,4 +29,4 @@ async def inline_query( geo_point=geo_point )) - return _tl.custom.InlineResults(self, result, entity=peer if entity else None) + return custom.InlineResults(self, result, entity=peer if entity else None) diff --git a/telethon/_client/buttons.py b/telethon/_client/buttons.py index 5dd9c413..897b4703 100644 --- a/telethon/_client/buttons.py +++ b/telethon/_client/buttons.py @@ -1,6 +1,8 @@ import typing -from .. import utils, hints, _tl +from .._misc import utils, hints +from .. import _tl +from .._tl import custom def build_reply_markup( @@ -30,7 +32,7 @@ def build_reply_markup( for row in buttons: current = [] for button in row: - if isinstance(button, _tl.custom.Button): + if isinstance(button, custom.Button): if button.resize is not None: resize = button.resize if button.single_use is not None: @@ -39,10 +41,10 @@ def build_reply_markup( selective = button.selective button = button.button - elif isinstance(button, _tl.custom.MessageButton): + elif isinstance(button, custom.MessageButton): button = button.button - inline = _tl.custom.Button._is_inline(button) + inline = custom.Button._is_inline(button) is_inline |= inline is_normal |= not inline diff --git a/telethon/_client/chats.py b/telethon/_client/chats.py index 904bba10..0fb88ed8 100644 --- a/telethon/_client/chats.py +++ b/telethon/_client/chats.py @@ -4,8 +4,9 @@ import itertools import string import typing -from .. import helpers, utils, hints, errors, _tl -from ..requestiter import RequestIter +from .. import hints, errors, _tl +from .._misc import helpers, utils, requestiter +from .._tl import custom if typing.TYPE_CHECKING: from .telegramclient import TelegramClient @@ -92,7 +93,7 @@ class _ChatAction: self._action.progress = 100 * round(current / total) -class _ParticipantsIter(RequestIter): +class _ParticipantsIter(requestiter.RequestIter): async def _init(self, entity, filter, search, aggressive): if isinstance(filter, type): if filter in (_tl.ChannelParticipantsBanned, @@ -246,7 +247,7 @@ class _ParticipantsIter(RequestIter): self.buffer.append(user) -class _AdminLogIter(RequestIter): +class _AdminLogIter(requestiter.RequestIter): async def _init( self, entity, admins, search, min_id, max_id, join, leave, invite, restrict, unrestrict, ban, unban, @@ -301,13 +302,13 @@ class _AdminLogIter(RequestIter): ev.action.message._finish_init( self.client, entities, self.entity) - self.buffer.append(_tl.custom.AdminLogEvent(ev, entities)) + self.buffer.append(custom.AdminLogEvent(ev, entities)) if len(r.events) < self.request.limit: return True -class _ProfilePhotoIter(RequestIter): +class _ProfilePhotoIter(requestiter.RequestIter): async def _init( self, entity, offset, max_id ): @@ -694,7 +695,7 @@ async def get_permissions( self: 'TelegramClient', entity: 'hints.EntityLike', user: 'hints.EntityLike' = None -) -> 'typing.Optional[_tl.custom.ParticipantPermissions]': +) -> 'typing.Optional[custom.ParticipantPermissions]': entity = await self.get_entity(entity) if not user: @@ -715,7 +716,7 @@ async def get_permissions( entity, user )) - return _tl.custom.ParticipantPermissions(participant.participant, False) + return custom.ParticipantPermissions(participant.participant, False) elif helpers._entity_type(entity) == helpers._EntityType.CHAT: chat = await self(_tl.fn.messages.GetFullChat( entity @@ -724,7 +725,7 @@ async def get_permissions( user = await self.get_me(input_peer=True) for participant in chat.full_chat.participants.participants: if participant.user_id == user.user_id: - return _tl.custom.ParticipantPermissions(participant, True) + return custom.ParticipantPermissions(participant, True) raise errors.UserNotParticipantError(None) raise ValueError('You must pass either a channel or a chat') diff --git a/telethon/_client/dialogs.py b/telethon/_client/dialogs.py index aee8861d..bfd76f61 100644 --- a/telethon/_client/dialogs.py +++ b/telethon/_client/dialogs.py @@ -3,8 +3,9 @@ import inspect import itertools import typing -from .. import helpers, utils, hints, errors, _tl -from ..requestiter import RequestIter +from .. import hints, errors, _tl +from .._misc import helpers, utils, requestiter +from .._tl import custom _MAX_CHUNK_SIZE = 100 @@ -23,7 +24,7 @@ def _dialog_message_key(peer, message_id): return (peer.channel_id if isinstance(peer, _tl.PeerChannel) else None), message_id -class _DialogsIter(RequestIter): +class _DialogsIter(requestiter.RequestIter): async def _init( self, offset_date, offset_id, offset_peer, ignore_pinned, ignore_migrated, folder ): @@ -79,7 +80,7 @@ class _DialogsIter(RequestIter): # Real world example: https://t.me/TelethonChat/271471 continue - cd = _tl.custom.Dialog(self.client, d, entities, message) + cd = custom.Dialog(self.client, d, entities, message) if cd.dialog.pts: self.client._channel_pts[cd.id] = cd.dialog.pts @@ -108,7 +109,7 @@ class _DialogsIter(RequestIter): self.request.offset_peer = self.buffer[-1].input_entity -class _DraftsIter(RequestIter): +class _DraftsIter(requestiter.RequestIter): async def _init(self, entities, **kwargs): if not entities: r = await self.client(_tl.fn.messages.GetAllDrafts()) @@ -127,7 +128,7 @@ class _DraftsIter(RequestIter): for x in itertools.chain(r.users, r.chats)} self.buffer.extend( - _tl.custom.Draft(self.client, entities[utils.get_peer_id(d.peer)], d.draft) + custom.Draft(self.client, entities[utils.get_peer_id(d.peer)], d.draft) for d in items ) diff --git a/telethon/_client/downloads.py b/telethon/_client/downloads.py index 974db2a0..dbb279f3 100644 --- a/telethon/_client/downloads.py +++ b/telethon/_client/downloads.py @@ -6,10 +6,9 @@ import typing import inspect import asyncio -from ..crypto import AES - -from .. import utils, helpers, errors, hints, _tl -from ..requestiter import RequestIter +from .._crypto import AES +from .._misc import utils, helpers, requestiter +from .. import errors, hints, _tl try: import aiohttp @@ -26,7 +25,7 @@ MAX_CHUNK_SIZE = 512 * 1024 # 2021-01-15, users reported that `errors.TimeoutError` can occur while downloading files. TIMED_OUT_SLEEP = 1 -class _DirectDownloadIter(RequestIter): +class _DirectDownloadIter(requestiter.RequestIter): async def _init( self, file, dc_id, offset, stride, chunk_size, request_size, file_size, msg_data ): diff --git a/telethon/_client/messages.py b/telethon/_client/messages.py index ba88c665..00ee418a 100644 --- a/telethon/_client/messages.py +++ b/telethon/_client/messages.py @@ -3,8 +3,8 @@ import itertools import typing import warnings -from .. import helpers, utils, errors, hints, _tl -from ..requestiter import RequestIter +from .. import errors, hints, _tl +from .._misc import helpers, utils, requestiter _MAX_CHUNK_SIZE = 100 @@ -12,7 +12,7 @@ if typing.TYPE_CHECKING: from .telegramclient import TelegramClient -class _MessagesIter(RequestIter): +class _MessagesIter(requestiter.RequestIter): """ Common factor for all requests that need to iterate over messages. """ @@ -263,7 +263,7 @@ class _MessagesIter(RequestIter): self.request.offset_rate = getattr(response, 'next_rate', 0) -class _IDsIter(RequestIter): +class _IDsIter(requestiter.RequestIter): async def _init(self, entity, ids): self.total = len(ids) self._ids = list(reversed(ids)) if self.reverse else ids diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 9c5d62d7..c89b1809 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -8,12 +8,10 @@ import time import typing from .. import version, helpers, __name__ as __base_name__, _tl -from ..crypto import rsa -from ..entitycache import EntityCache -from ..extensions import markdown -from ..network import MTProtoSender, Connection, ConnectionTcpFull, TcpMTProxy +from .._crypto import rsa +from .._misc import markdown, entitycache, statecache +from .._network import MTProtoSender, Connection, ConnectionTcpFull, TcpMTProxy from ..sessions import Session, SQLiteSession, MemorySession -from ..statecache import StateCache DEFAULT_DC_ID = 2 DEFAULT_IPV4_IP = '149.154.167.51' @@ -151,7 +149,7 @@ def init( # TODO Session should probably return all cached # info of entities, not just the input versions self.session = session - self._entity_cache = EntityCache() + self._entity_cache = entitycache.EntityCache() self.api_id = int(api_id) self.api_hash = api_hash @@ -259,7 +257,7 @@ def init( # Update state (for catching up after a disconnection) # TODO Get state from channels too - self._state_cache = StateCache( + self._state_cache = statecache.StateCache( self.session.get_update_state(0), self._log) # Some further state for subclasses diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index d3935273..986b89f0 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -9,7 +9,8 @@ from . import ( telegrambaseclient, updates, uploads, users ) from .. import helpers, version, _tl -from ..network import ConnectionTcpFull +from .._tl import custom +from .._network import ConnectionTcpFull from ..events.common import EventBuilder, EventCommon @@ -3390,7 +3391,7 @@ class TelegramClient: await client.send_file(chat, file, progress_callback=callback) # Dices, including dart and other future emoji - from telethon.tl import types + from telethon import _tl await client.send_file(chat, _tl.InputMediaDice('')) await client.send_file(chat, _tl.InputMediaDice('🎯')) diff --git a/telethon/_client/uploads.py b/telethon/_client/uploads.py index 6c0d8146..db2cdd77 100644 --- a/telethon/_client/uploads.py +++ b/telethon/_client/uploads.py @@ -7,9 +7,11 @@ import re import typing from io import BytesIO -from ..crypto import AES +from .._crypto import AES -from .. import utils, helpers, hints, _tl +from .._misc import utils, helpers +from .. import hints, _tl +from .._tl import custom try: import PIL @@ -361,7 +363,7 @@ async def upload_file( if is_big: return _tl.InputFileBig(file_id, part_count, file_name) else: - return _tl.custom.InputSizedFile( + return custom.InputSizedFile( file_id, part_count, file_name, md5=hash_md5, size=file_size ) diff --git a/telethon/_client/users.py b/telethon/_client/users.py index 6209619a..e493ea61 100644 --- a/telethon/_client/users.py +++ b/telethon/_client/users.py @@ -4,9 +4,9 @@ import itertools import time import typing -from .. import errors, helpers, utils, hints, _tl +from .. import errors, hints, _tl +from .._misc import helpers, utils from ..errors import MultiError, RPCError -from ..helpers import retry_range _NOT_A_REQUEST = lambda: TypeError('You can only invoke requests, not types!') @@ -53,7 +53,7 @@ async def call(self: 'TelegramClient', sender, request, ordered=False, flood_sle last_error = None self._last_request = time.time() - for attempt in retry_range(self._request_retries): + for attempt in helpers.retry_range(self._request_retries): try: future = sender.send(request, ordered=ordered) if isinstance(future, list): diff --git a/telethon/_crypto/authkey.py b/telethon/_crypto/authkey.py index 8475ec17..fa6fbb78 100644 --- a/telethon/_crypto/authkey.py +++ b/telethon/_crypto/authkey.py @@ -4,7 +4,7 @@ This module holds the AuthKey class. import struct from hashlib import sha1 -from ..extensions import BinaryReader +from .._misc import BinaryReader class AuthKey: diff --git a/telethon/_crypto/cdndecrypter.py b/telethon/_crypto/cdndecrypter.py index efdc3288..8347a561 100644 --- a/telethon/_crypto/cdndecrypter.py +++ b/telethon/_crypto/cdndecrypter.py @@ -4,7 +4,7 @@ This module holds the CdnDecrypter utility class. from hashlib import sha256 from .. import _tl -from ..crypto import AESModeCTR +from .._crypto import AESModeCTR from ..errors import CdnFileTamperedError diff --git a/telethon/_misc/entitycache.py b/telethon/_misc/entitycache.py index f3116b7d..b6f87697 100644 --- a/telethon/_misc/entitycache.py +++ b/telethon/_misc/entitycache.py @@ -1,7 +1,8 @@ import inspect import itertools -from . import utils, _tl +from .._misc import utils +from .. import _tl # Which updates have the following fields? _has_field = { diff --git a/telethon/_misc/hints.py b/telethon/_misc/hints.py index 7b1ec5ae..a5299a25 100644 --- a/telethon/_misc/hints.py +++ b/telethon/_misc/hints.py @@ -1,7 +1,9 @@ import datetime import typing -from . import helpers, _tl +from . import helpers +from .. import _tl +from .._tl import custom Phone = str Username = str diff --git a/telethon/_misc/markdown.py b/telethon/_misc/markdown.py index 336da0b9..b9661af3 100644 --- a/telethon/_misc/markdown.py +++ b/telethon/_misc/markdown.py @@ -6,7 +6,7 @@ since they seem to count as two characters and it's a bit strange. import re import warnings -from ..helpers import add_surrogate, del_surrogate, within_surrogate, strip_text +from .helpers import add_surrogate, del_surrogate, within_surrogate, strip_text from .. import _tl DEFAULT_DELIMITERS = { diff --git a/telethon/_misc/messagepacker.py b/telethon/_misc/messagepacker.py index c0f46f48..f4efb1ac 100644 --- a/telethon/_misc/messagepacker.py +++ b/telethon/_misc/messagepacker.py @@ -3,9 +3,9 @@ import collections import io import struct -from ..tl import TLRequest -from ..tl.core.messagecontainer import MessageContainer -from ..tl.core.tlmessage import TLMessage +from .._tl import TLRequest +from .._tl.core.messagecontainer import MessageContainer +from .._tl.core.tlmessage import TLMessage class MessagePacker: diff --git a/telethon/_misc/password.py b/telethon/_misc/password.py index e02c8eb8..b18e6b10 100644 --- a/telethon/_misc/password.py +++ b/telethon/_misc/password.py @@ -1,8 +1,8 @@ import hashlib import os -from .crypto import factorization -from . import _tl +from .._crypto import factorization +from .. import _tl def check_prime_and_good_check(prime: int, g: int): diff --git a/telethon/_misc/statecache.py b/telethon/_misc/statecache.py index 3f2475bf..7f3ddf59 100644 --- a/telethon/_misc/statecache.py +++ b/telethon/_misc/statecache.py @@ -1,6 +1,6 @@ import inspect -from . import _tl +from .. import _tl # Which updates have the following fields? diff --git a/telethon/_misc/utils.py b/telethon/_misc/utils.py index 9826e551..956a154d 100644 --- a/telethon/_misc/utils.py +++ b/telethon/_misc/utils.py @@ -19,9 +19,9 @@ from collections import namedtuple from mimetypes import guess_extension from types import GeneratorType -from .extensions import markdown, html from .helpers import add_surrogate, del_surrogate, strip_text -from . import _tl +from . import markdown, html +from .. import _tl try: import hachoir @@ -32,26 +32,26 @@ except ImportError: # Register some of the most common mime-types to avoid any issues. # See https://github.com/LonamiWebs/Telethon/issues/1096. -mime_tl.add_type('image/png', '.png') -mime_tl.add_type('image/jpeg', '.jpeg') -mime_tl.add_type('image/webp', '.webp') -mime_tl.add_type('image/gif', '.gif') -mime_tl.add_type('image/bmp', '.bmp') -mime_tl.add_type('image/x-tga', '.tga') -mime_tl.add_type('image/tiff', '.tiff') -mime_tl.add_type('image/vnd.adobe.photoshop', '.psd') +mimetypes.add_type('image/png', '.png') +mimetypes.add_type('image/jpeg', '.jpeg') +mimetypes.add_type('image/webp', '.webp') +mimetypes.add_type('image/gif', '.gif') +mimetypes.add_type('image/bmp', '.bmp') +mimetypes.add_type('image/x-tga', '.tga') +mimetypes.add_type('image/tiff', '.tiff') +mimetypes.add_type('image/vnd.adobe.photoshop', '.psd') -mime_tl.add_type('video/mp4', '.mp4') -mime_tl.add_type('video/quicktime', '.mov') -mime_tl.add_type('video/avi', '.avi') +mimetypes.add_type('video/mp4', '.mp4') +mimetypes.add_type('video/quicktime', '.mov') +mimetypes.add_type('video/avi', '.avi') -mime_tl.add_type('audio/mpeg', '.mp3') -mime_tl.add_type('audio/m4a', '.m4a') -mime_tl.add_type('audio/aac', '.aac') -mime_tl.add_type('audio/ogg', '.ogg') -mime_tl.add_type('audio/flac', '.flac') +mimetypes.add_type('audio/mpeg', '.mp3') +mimetypes.add_type('audio/m4a', '.m4a') +mimetypes.add_type('audio/aac', '.aac') +mimetypes.add_type('audio/ogg', '.ogg') +mimetypes.add_type('audio/flac', '.flac') -mime_tl.add_type('application/x-tgsticker', '.tgs') +mimetypes.add_type('application/x-tgsticker', '.tgs') USERNAME_RE = re.compile( r'@|(?:https?://)?(?:www\.)?(?:telegram\.(?:me|dog)|t\.me)/(@|joinchat/)?' @@ -675,7 +675,7 @@ def get_attributes(file, *, attributes=None, mime_type=None, # Note: ``file.name`` works for :tl:`InputFile` and some `IOBase` streams name = file if isinstance(file, str) else getattr(file, 'name', 'unnamed') if mime_type is None: - mime_type = mime_tl.guess_type(name)[0] + mime_type = mimetypes.guess_type(name)[0] attr_dict = {_tl.DocumentAttributeFilename: _tl.DocumentAttributeFilename(os.path.basename(name))} @@ -881,7 +881,7 @@ def is_audio(file): return False else: file = 'a' + ext - return (mime_tl.guess_type(file)[0] or '').startswith('audio/') + return (mimetypes.guess_type(file)[0] or '').startswith('audio/') def is_video(file): @@ -895,7 +895,7 @@ def is_video(file): return False else: file = 'a' + ext - return (mime_tl.guess_type(file)[0] or '').startswith('video/') + return (mimetypes.guess_type(file)[0] or '').startswith('video/') def is_list_like(obj): diff --git a/telethon/_network/authenticator.py b/telethon/_network/authenticator.py index 04b6f5e3..f5b3591c 100644 --- a/telethon/_network/authenticator.py +++ b/telethon/_network/authenticator.py @@ -7,9 +7,9 @@ import time from hashlib import sha1 from .. import helpers, _tl -from ..crypto import AES, AuthKey, Factorization, rsa +from .._crypto import AES, AuthKey, Factorization, rsa from ..errors import SecurityError -from ..extensions import BinaryReader +from .._misc import BinaryReader async def do_authentication(sender): diff --git a/telethon/_network/connection/tcpmtproxy.py b/telethon/_network/connection/tcpmtproxy.py index 69a43bce..db18a61c 100644 --- a/telethon/_network/connection/tcpmtproxy.py +++ b/telethon/_network/connection/tcpmtproxy.py @@ -9,7 +9,7 @@ from .tcpintermediate import ( RandomizedIntermediatePacketCodec ) -from ...crypto import AESModeCTR +from ..._crypto import AESModeCTR class MTProxyIO: diff --git a/telethon/_network/connection/tcpobfuscated.py b/telethon/_network/connection/tcpobfuscated.py index cf2e6af5..2aeeeac1 100644 --- a/telethon/_network/connection/tcpobfuscated.py +++ b/telethon/_network/connection/tcpobfuscated.py @@ -3,7 +3,7 @@ import os from .tcpabridged import AbridgedPacketCodec from .connection import ObfuscatedConnection -from ...crypto import AESModeCTR +from ..._crypto import AESModeCTR class ObfuscatedIO: diff --git a/telethon/_network/mtprotoplainsender.py b/telethon/_network/mtprotoplainsender.py index 563affd7..433c3795 100644 --- a/telethon/_network/mtprotoplainsender.py +++ b/telethon/_network/mtprotoplainsender.py @@ -6,7 +6,7 @@ import struct from .mtprotostate import MTProtoState from ..errors import InvalidBufferError -from ..extensions import BinaryReader +from .._misc import BinaryReader class MTProtoPlainSender: diff --git a/telethon/_network/mtprotosender.py b/telethon/_network/mtprotosender.py index ca592ac0..59f14a7a 100644 --- a/telethon/_network/mtprotosender.py +++ b/telethon/_network/mtprotosender.py @@ -3,27 +3,20 @@ import collections import struct from . import authenticator -from ..extensions.messagepacker import MessagePacker +from .._misc.messagepacker import MessagePacker from .mtprotoplainsender import MTProtoPlainSender from .requeststate import RequestState from .mtprotostate import MTProtoState -from ..tl.tlobject import TLRequest -from .. import helpers, utils +from .._tl.tlobject import TLRequest +from .. import helpers, utils, _tl from ..errors import ( BadMessageError, InvalidBufferError, SecurityError, TypeNotFoundError, rpc_message_to_error ) -from ..extensions import BinaryReader -from ..tl.core import RpcResult, MessageContainer, GzipPacked -from ..tl.functions.auth import LogOutRequest -from ..tl.functions import PingRequest, DestroySessionRequest -from ..tl.types import ( - MsgsAck, Pong, BadServerSalt, BadMsgNotification, FutureSalts, - MsgNewDetailedInfo, NewSessionCreated, MsgDetailedInfo, MsgsStateReq, - MsgsStateInfo, MsgsAllInfo, MsgResendReq, upload, DestroySessionOk, DestroySessionNone, -) -from ..crypto import AuthKey -from ..helpers import retry_range +from .._misc import BinaryReader +from .._tl.core import RpcResult, MessageContainer, GzipPacked +from .._crypto import AuthKey +from .._misc.helpers import retry_range class MTProtoSender: @@ -97,19 +90,19 @@ class MTProtoSender: RpcResult.CONSTRUCTOR_ID: self._handle_rpc_result, MessageContainer.CONSTRUCTOR_ID: self._handle_container, GzipPacked.CONSTRUCTOR_ID: self._handle_gzip_packed, - Pong.CONSTRUCTOR_ID: self._handle_pong, - BadServerSalt.CONSTRUCTOR_ID: self._handle_bad_server_salt, - BadMsgNotification.CONSTRUCTOR_ID: self._handle_bad_notification, - MsgDetailedInfo.CONSTRUCTOR_ID: self._handle_detailed_info, - MsgNewDetailedInfo.CONSTRUCTOR_ID: self._handle_new_detailed_info, - NewSessionCreated.CONSTRUCTOR_ID: self._handle_new_session_created, - MsgsAck.CONSTRUCTOR_ID: self._handle_ack, - FutureSalts.CONSTRUCTOR_ID: self._handle_future_salts, - MsgsStateReq.CONSTRUCTOR_ID: self._handle_state_forgotten, - MsgResendReq.CONSTRUCTOR_ID: self._handle_state_forgotten, - MsgsAllInfo.CONSTRUCTOR_ID: self._handle_msg_all, - DestroySessionOk: self._handle_destroy_session, - DestroySessionNone: self._handle_destroy_session, + _tl.Pong.CONSTRUCTOR_ID: self._handle_pong, + _tl.BadServerSalt.CONSTRUCTOR_ID: self._handle_bad_server_salt, + _tl.BadMsgNotification.CONSTRUCTOR_ID: self._handle_bad_notification, + _tl.MsgDetailedInfo.CONSTRUCTOR_ID: self._handle_detailed_info, + _tl.MsgNewDetailedInfo.CONSTRUCTOR_ID: self._handle_new_detailed_info, + _tl.NewSessionCreated.CONSTRUCTOR_ID: self._handle_new_session_created, + _tl.MsgsAck.CONSTRUCTOR_ID: self._handle_ack, + _tl.FutureSalts.CONSTRUCTOR_ID: self._handle_future_salts, + _tl.MsgsStateReq.CONSTRUCTOR_ID: self._handle_state_forgotten, + _tl.MsgResendReq.CONSTRUCTOR_ID: self._handle_state_forgotten, + _tl.MsgsAllInfo.CONSTRUCTOR_ID: self._handle_msg_all, + _tl.DestroySessionOk: self._handle_destroy_session, + _tl.DestroySessionNone: self._handle_destroy_session, } # Public API @@ -433,7 +426,7 @@ class MTProtoSender: # TODO this is ugly, update loop shouldn't worry about this, sender should if self._ping is None: self._ping = rnd_id - self.send(PingRequest(rnd_id)) + self.send(_tl.fn.Ping(rnd_id)) else: self._start_reconnect(None) @@ -448,7 +441,7 @@ class MTProtoSender: """ while self._user_connected and not self._reconnecting: if self._pending_ack: - ack = RequestState(MsgsAck(list(self._pending_ack))) + ack = RequestState(_tl.MsgsAck(list(self._pending_ack))) self._send_queue.append(ack) self._last_acks.append(ack) self._pending_ack.clear() @@ -598,7 +591,7 @@ class MTProtoSender: # which contain the real response right after. try: with BinaryReader(rpc_result.body) as reader: - if not isinstance(reader.tgread_object(), upload.File): + if not isinstance(reader.tgread_object(), _tl.upload.File): raise ValueError('Not an upload.File') except (TypeNotFoundError, ValueError): self._log.info('Received response without parent request: %s', rpc_result.body) @@ -607,7 +600,7 @@ class MTProtoSender: if rpc_result.error: error = rpc_message_to_error(rpc_result.error, state.request) self._send_queue.append( - RequestState(MsgsAck([state.msg_id]))) + RequestState(_tl.MsgsAck([state.msg_id]))) if not state.future.cancelled(): state.future.set_exception(error) @@ -777,7 +770,7 @@ class MTProtoSender: self._log.debug('Handling acknowledge for %s', str(ack.msg_ids)) for msg_id in ack.msg_ids: state = self._pending_state.get(msg_id) - if state and isinstance(state.request, LogOutRequest): + if state and isinstance(state.request, _tl.fn.auth.LogOut): del self._pending_state[msg_id] if not state.future.cancelled(): state.future.set_result(True) @@ -802,7 +795,7 @@ class MTProtoSender: Handles both :tl:`MsgsStateReq` and :tl:`MsgResendReq` by enqueuing a :tl:`MsgsStateInfo` to be sent at a later point. """ - self._send_queue.append(RequestState(MsgsStateInfo( + self._send_queue.append(RequestState(_tl.MsgsStateInfo( req_msg_id=message.msg_id, info=chr(1) * len(message.obj.msg_ids) ))) @@ -817,7 +810,7 @@ class MTProtoSender: It behaves pretty much like handling an RPC result. """ for msg_id, state in self._pending_state.items(): - if isinstance(state.request, DestroySessionRequest)\ + if isinstance(state.request, _tl.fn.DestroySession)\ and state.request.session_id == message.obj.session_id: break else: diff --git a/telethon/_network/mtprotostate.py b/telethon/_network/mtprotostate.py index 0fe9cc08..e2c6c21a 100644 --- a/telethon/_network/mtprotostate.py +++ b/telethon/_network/mtprotostate.py @@ -3,13 +3,13 @@ import struct import time from hashlib import sha256 -from ..crypto import AES +from .._crypto import AES from ..errors import SecurityError, InvalidBufferError -from ..extensions import BinaryReader -from ..tl.core import TLMessage -from ..tl.tlobject import TLRequest -from ..tl.functions import InvokeAfterMsgRequest -from ..tl.core.gzippacked import GzipPacked +from .._misc import BinaryReader +from .._tl.core import TLMessage +from .._tl.tlobject import TLRequest +from .. import _tl +from .._tl.core.gzippacked import GzipPacked class _OpaqueRequest(TLRequest): @@ -103,7 +103,7 @@ class MTProtoState: # The `RequestState` stores `bytes(request)`, not the request itself. # `invokeAfterMsg` wants a `TLRequest` though, hence the wrapping. body = GzipPacked.gzip_if_smaller(content_related, - bytes(InvokeAfterMsgRequest(after_id, _OpaqueRequest(data)))) + bytes(_tl.fn.InvokeAfterMsgRequest(after_id, _OpaqueRequest(data)))) buffer.write(struct.pack('`). """ - return isinstance(self._chat_peer, types.PeerUser) if self._chat_peer else None + return isinstance(self._chat_peer, _tl.PeerUser) if self._chat_peer else None @property def is_group(self): @@ -128,20 +127,20 @@ class ChatGetter(abc.ABC): if self._broadcast is None and hasattr(self.chat, 'broadcast'): self._broadcast = bool(self.chat.broadcast) - if isinstance(self._chat_peer, types.PeerChannel): + if isinstance(self._chat_peer, _tl.PeerChannel): if self._broadcast is None: return None else: return not self._broadcast - return isinstance(self._chat_peer, types.PeerChat) + return isinstance(self._chat_peer, _tl.PeerChat) @property def is_channel(self): """`True` if the message was sent on a megagroup or channel.""" # The only case where chat peer could be none is in MessageDeleted, # however those always have the peer in channels. - return isinstance(self._chat_peer, types.PeerChannel) + return isinstance(self._chat_peer, _tl.PeerChannel) async def _refetch_chat(self): """ diff --git a/telethon/_tl/custom/dialog.py b/telethon/_tl/custom/dialog.py index 955bbdf2..cc307b4b 100644 --- a/telethon/_tl/custom/dialog.py +++ b/telethon/_tl/custom/dialog.py @@ -1,6 +1,6 @@ from . import Draft -from .. import TLObject, types, functions -from ... import utils +from ... import _tl +from ..._misc import utils class Dialog: @@ -89,12 +89,12 @@ class Dialog: self.draft = Draft(client, self.entity, self.dialog.draft) - self.is_user = isinstance(self.entity, types.User) + self.is_user = isinstance(self.entity, _tl.User) self.is_group = ( - isinstance(self.entity, (types.Chat, types.ChatForbidden)) or - (isinstance(self.entity, types.Channel) and self.entity.megagroup) + isinstance(self.entity, (_tl.Chat, _tl.ChatForbidden)) or + (isinstance(self.entity, _tl.Channel) and self.entity.megagroup) ) - self.is_channel = isinstance(self.entity, types.Channel) + self.is_channel = isinstance(self.entity, _tl.Channel) async def send_message(self, *args, **kwargs): """ @@ -141,7 +141,7 @@ class Dialog: dialog.archive(0) """ return await self._client(_tl.fn.folders.EditPeerFolders([ - types.InputFolderPeer(self.input_entity, folder_id=folder) + _tl.InputFolderPeer(self.input_entity, folder_id=folder) ])) def to_dict(self): @@ -155,7 +155,7 @@ class Dialog: } def __str__(self): - return TLObject.pretty_format(self.to_dict()) + return _tl.TLObject.pretty_format(self.to_dict()) def stringify(self): - return TLObject.pretty_format(self.to_dict(), indent=0) + return _tl.TLObject.pretty_format(self.to_dict(), indent=0) diff --git a/telethon/_tl/custom/draft.py b/telethon/_tl/custom/draft.py index ed6360b0..fb1df26f 100644 --- a/telethon/_tl/custom/draft.py +++ b/telethon/_tl/custom/draft.py @@ -2,8 +2,8 @@ import datetime from ... import _tl from ...errors import RPCError -from ...extensions import markdown -from ...utils import get_input_peer, get_peer +from ..._misc import markdown +from ..._misc.utils import get_input_peer, get_peer class Draft: diff --git a/telethon/_tl/custom/file.py b/telethon/_tl/custom/file.py index 210eb53d..228727ea 100644 --- a/telethon/_tl/custom/file.py +++ b/telethon/_tl/custom/file.py @@ -1,8 +1,8 @@ import mimetypes import os -from ... import utils -from ...tl import types +from ..._misc import utils +from ... import _tl class File: @@ -38,7 +38,7 @@ class File: """ The file name of this document. """ - return self._from_attr(types.DocumentAttributeFilename, 'file_name') + return self._from_attr(_tl.DocumentAttributeFilename, 'file_name') @property def ext(self): @@ -49,7 +49,7 @@ class File: from the file name (if any) will be used. """ return ( - mimetypes.guess_extension(self.mime_type) + mime_tl.guess_extension(self.mime_type) or os.path.splitext(self.name or '')[-1] or None ) @@ -59,9 +59,9 @@ class File: """ The mime-type of this file. """ - if isinstance(self.media, types.Photo): + if isinstance(self.media, _tl.Photo): return 'image/jpeg' - elif isinstance(self.media, types.Document): + elif isinstance(self.media, _tl.Document): return self.media.mime_type @property @@ -69,22 +69,22 @@ class File: """ The width in pixels of this media if it's a photo or a video. """ - if isinstance(self.media, types.Photo): + if isinstance(self.media, _tl.Photo): return max(getattr(s, 'w', 0) for s in self.media.sizes) return self._from_attr(( - types.DocumentAttributeImageSize, types.DocumentAttributeVideo), 'w') + _tl.DocumentAttributeImageSize, _tl.DocumentAttributeVideo), 'w') @property def height(self): """ The height in pixels of this media if it's a photo or a video. """ - if isinstance(self.media, types.Photo): + if isinstance(self.media, _tl.Photo): return max(getattr(s, 'h', 0) for s in self.media.sizes) return self._from_attr(( - types.DocumentAttributeImageSize, types.DocumentAttributeVideo), 'h') + _tl.DocumentAttributeImageSize, _tl.DocumentAttributeVideo), 'h') @property def duration(self): @@ -92,35 +92,35 @@ class File: The duration in seconds of the audio or video. """ return self._from_attr(( - types.DocumentAttributeAudio, types.DocumentAttributeVideo), 'duration') + _tl.DocumentAttributeAudio, _tl.DocumentAttributeVideo), 'duration') @property def title(self): """ The title of the song. """ - return self._from_attr(types.DocumentAttributeAudio, 'title') + return self._from_attr(_tl.DocumentAttributeAudio, 'title') @property def performer(self): """ The performer of the song. """ - return self._from_attr(types.DocumentAttributeAudio, 'performer') + return self._from_attr(_tl.DocumentAttributeAudio, 'performer') @property def emoji(self): """ A string with all emoji that represent the current sticker. """ - return self._from_attr(types.DocumentAttributeSticker, 'alt') + return self._from_attr(_tl.DocumentAttributeSticker, 'alt') @property def sticker_set(self): """ The :tl:`InputStickerSet` to which the sticker file belongs. """ - return self._from_attr(types.DocumentAttributeSticker, 'stickerset') + return self._from_attr(_tl.DocumentAttributeSticker, 'stickerset') @property def size(self): @@ -129,13 +129,13 @@ class File: For photos, this is the heaviest thumbnail, as it often repressents the largest dimensions. """ - if isinstance(self.media, types.Photo): + if isinstance(self.media, _tl.Photo): return max(filter(None, map(utils._photo_size_byte_count, self.media.sizes)), default=None) - elif isinstance(self.media, types.Document): + elif isinstance(self.media, _tl.Document): return self.media.size def _from_attr(self, cls, field): - if isinstance(self.media, types.Document): + if isinstance(self.media, _tl.Document): for attr in self.media.attributes: if isinstance(attr, cls): return getattr(attr, field, None) diff --git a/telethon/_tl/custom/forward.py b/telethon/_tl/custom/forward.py index a95eae30..d6a46cb7 100644 --- a/telethon/_tl/custom/forward.py +++ b/telethon/_tl/custom/forward.py @@ -1,7 +1,6 @@ from .chatgetter import ChatGetter from .sendergetter import SenderGetter -from ... import utils, helpers -from ...tl import types +from ..._misc import utils, helpers class Forward(ChatGetter, SenderGetter): diff --git a/telethon/_tl/custom/inlinebuilder.py b/telethon/_tl/custom/inlinebuilder.py index f3851f35..b401ab04 100644 --- a/telethon/_tl/custom/inlinebuilder.py +++ b/telethon/_tl/custom/inlinebuilder.py @@ -1,7 +1,7 @@ import hashlib -from .. import functions, types -from ... import utils +from ... import _tl +from ..._misc import utils _TYPE_TO_MIMES = { 'gif': ['image/gif'], # 'video/mp4' too, but that's used for video @@ -126,7 +126,7 @@ class InlineBuilder: # TODO Does 'article' work always? # article, photo, gif, mpeg4_gif, video, audio, # voice, document, location, venue, contact, game - result = types.InputBotInlineResult( + result = _tl.InputBotInlineResult( id=id or '', type='article', send_message=await self._message( @@ -194,15 +194,15 @@ class InlineBuilder: _, media, _ = await self._client._file_to_media( file, allow_cache=True, as_image=True ) - if isinstance(media, types.InputPhoto): + if isinstance(media, _tl.InputPhoto): fh = media else: r = await self._client(_tl.fn.messages.UploadMedia( - types.InputPeerSelf(), media=media + _tl.InputPeerSelf(), media=media )) fh = utils.get_input_photo(r.photo) - result = types.InputBotInlineResultPhoto( + result = _tl.InputBotInlineResultPhoto( id=id or '', type='photo', photo=fh, @@ -314,15 +314,15 @@ class InlineBuilder: video_note=video_note, allow_cache=use_cache ) - if isinstance(media, types.InputDocument): + if isinstance(media, _tl.InputDocument): fh = media else: r = await self._client(_tl.fn.messages.UploadMedia( - types.InputPeerSelf(), media=media + _tl.InputPeerSelf(), media=media )) fh = utils.get_input_document(r.document) - result = types.InputBotInlineResultDocument( + result = _tl.InputBotInlineResultDocument( id=id or '', type=type, document=fh, @@ -361,7 +361,7 @@ class InlineBuilder: short_name (`str`): The short name of the game to use. """ - result = types.InputBotInlineResultGame( + result = _tl.InputBotInlineResultGame( id=id or '', short_name=short_name, send_message=await self._message( @@ -400,31 +400,31 @@ class InlineBuilder: # "MediaAuto" means it will use whatever media the inline # result itself has (stickers, photos, or documents), while # respecting the user's text (caption) and formatting. - return types.InputBotInlineMessageMediaAuto( + return _tl.InputBotInlineMessageMediaAuto( message=text, entities=msg_entities, reply_markup=markup ) else: - return types.InputBotInlineMessageText( + return _tl.InputBotInlineMessageText( message=text, no_webpage=not link_preview, entities=msg_entities, reply_markup=markup ) - elif isinstance(geo, (types.InputGeoPoint, types.GeoPoint)): - return types.InputBotInlineMessageMediaGeo( + elif isinstance(geo, (_tl.InputGeoPoint, _tl.GeoPoint)): + return _tl.InputBotInlineMessageMediaGeo( geo_point=utils.get_input_geo(geo), period=period, reply_markup=markup ) - elif isinstance(geo, (types.InputMediaVenue, types.MessageMediaVenue)): - if isinstance(geo, types.InputMediaVenue): + elif isinstance(geo, (_tl.InputMediaVenue, _tl.MessageMediaVenue)): + if isinstance(geo, _tl.InputMediaVenue): geo_point = geo.geo_point else: geo_point = geo.geo - return types.InputBotInlineMessageMediaVenue( + return _tl.InputBotInlineMessageMediaVenue( geo_point=geo_point, title=geo.title, address=geo.address, @@ -434,8 +434,8 @@ class InlineBuilder: reply_markup=markup ) elif isinstance(contact, ( - types.InputMediaContact, types.MessageMediaContact)): - return types.InputBotInlineMessageMediaContact( + _tl.InputMediaContact, _tl.MessageMediaContact)): + return _tl.InputBotInlineMessageMediaContact( phone_number=contact.phone_number, first_name=contact.first_name, last_name=contact.last_name, @@ -443,7 +443,7 @@ class InlineBuilder: reply_markup=markup ) elif game: - return types.InputBotInlineMessageGame( + return _tl.InputBotInlineMessageGame( reply_markup=markup ) else: diff --git a/telethon/_tl/custom/inlineresult.py b/telethon/_tl/custom/inlineresult.py index eefbd7b4..fa617af1 100644 --- a/telethon/_tl/custom/inlineresult.py +++ b/telethon/_tl/custom/inlineresult.py @@ -1,5 +1,5 @@ -from .. import types, functions -from ... import utils +from ... import _tl +from ..._misc import utils class InlineResult: diff --git a/telethon/_tl/custom/inputsizedfile.py b/telethon/_tl/custom/inputsizedfile.py index fcb743f6..4183ecb7 100644 --- a/telethon/_tl/custom/inputsizedfile.py +++ b/telethon/_tl/custom/inputsizedfile.py @@ -1,7 +1,7 @@ -from ..types import InputFile +from ... import _tl -class InputSizedFile(InputFile): +class InputSizedFile(_tl.InputFile): """InputFile class with two extra parameters: md5 (digest) and size""" def __init__(self, id_, parts, name, md5, size): super().__init__(id_, parts, name, md5.hexdigest()) diff --git a/telethon/_tl/custom/message.py b/telethon/_tl/custom/message.py index fb134ac3..7672de2c 100644 --- a/telethon/_tl/custom/message.py +++ b/telethon/_tl/custom/message.py @@ -5,13 +5,13 @@ from .sendergetter import SenderGetter from .messagebutton import MessageButton from .forward import Forward from .file import File -from .. import TLObject, types, functions, alltlobjects -from ... import utils, errors +from ..._misc import utils +from ... import errors, _tl # TODO Figure out a way to have the code generator error on missing fields # Maybe parsing the init function alone if that's possible. -class Message(ChatGetter, SenderGetter, TLObject): +class Message(ChatGetter, SenderGetter, _tl.TLObject): """ This custom class aggregates both :tl:`Message` and :tl:`MessageService` to ease accessing their members. @@ -163,7 +163,7 @@ class Message(ChatGetter, SenderGetter, TLObject): self, id: int, # Common to Message and MessageService (mandatory) - peer_id: types.TypePeer = None, + peer_id: _tl.TypePeer = None, date: Optional[datetime] = None, # Common to Message and MessageService (flags) @@ -172,19 +172,19 @@ class Message(ChatGetter, SenderGetter, TLObject): media_unread: Optional[bool] = None, silent: Optional[bool] = None, post: Optional[bool] = None, - from_id: Optional[types.TypePeer] = None, - reply_to: Optional[types.TypeMessageReplyHeader] = None, + from_id: Optional[_tl.TypePeer] = None, + reply_to: Optional[_tl.TypeMessageReplyHeader] = None, ttl_period: Optional[int] = None, # For Message (mandatory) message: Optional[str] = None, # For Message (flags) - fwd_from: Optional[types.TypeMessageFwdHeader] = None, + fwd_from: Optional[_tl.TypeMessageFwdHeader] = None, via_bot_id: Optional[int] = None, - media: Optional[types.TypeMessageMedia] = None, - reply_markup: Optional[types.TypeReplyMarkup] = None, - entities: Optional[List[types.TypeMessageEntity]] = None, + media: Optional[_tl.TypeMessageMedia] = None, + reply_markup: Optional[_tl.TypeReplyMarkup] = None, + entities: Optional[List[_tl.TypeMessageEntity]] = None, views: Optional[int] = None, edit_date: Optional[datetime] = None, post_author: Optional[str] = None, @@ -193,12 +193,12 @@ class Message(ChatGetter, SenderGetter, TLObject): legacy: Optional[bool] = None, edit_hide: Optional[bool] = None, pinned: Optional[bool] = None, - restriction_reason: Optional[types.TypeRestrictionReason] = None, + restriction_reason: Optional[_tl.TypeRestrictionReason] = None, forwards: Optional[int] = None, - replies: Optional[types.TypeMessageReplies] = None, + replies: Optional[_tl.TypeMessageReplies] = None, # For MessageAction (mandatory) - action: Optional[types.TypeMessageAction] = None + action: Optional[_tl.TypeMessageAction] = None ): # Common properties to messages, then to service (in the order they're defined in the `.tl`) self.out = bool(out) @@ -217,7 +217,7 @@ class Message(ChatGetter, SenderGetter, TLObject): self.reply_to = reply_to self.date = date self.message = message - self.media = None if isinstance(media, types.MessageMediaEmpty) else media + self.media = None if isinstance(media, _tl.MessageMediaEmpty) else media self.reply_markup = reply_markup self.entities = entities self.views = views @@ -253,7 +253,7 @@ class Message(ChatGetter, SenderGetter, TLObject): # ...or... # incoming messages in private conversations no longer have from_id # (layer 119+), but the sender can only be the chat we're in. - if post or (not out and isinstance(peer_id, types.PeerUser)): + if post or (not out and isinstance(peer_id, _tl.PeerUser)): sender_id = utils.get_peer_id(peer_id) # Note that these calls would reset the client @@ -272,7 +272,7 @@ class Message(ChatGetter, SenderGetter, TLObject): # Make messages sent to ourselves outgoing unless they're forwarded. # This makes it consistent with official client's appearance. - if self.peer_id == types.PeerUser(client._self_id) and not self.fwd_from: + if self.peer_id == _tl.PeerUser(client._self_id) and not self.fwd_from: self.out = True cache = client._entity_cache @@ -294,25 +294,25 @@ class Message(ChatGetter, SenderGetter, TLObject): self._forward = Forward(self._client, self.fwd_from, entities) if self.action: - if isinstance(self.action, (types.MessageActionChatAddUser, - types.MessageActionChatCreate)): + if isinstance(self.action, (_tl.MessageActionChatAddUser, + _tl.MessageActionChatCreate)): self._action_entities = [entities.get(i) for i in self.action.users] - elif isinstance(self.action, types.MessageActionChatDeleteUser): + elif isinstance(self.action, _tl.MessageActionChatDeleteUser): self._action_entities = [entities.get(self.action.user_id)] - elif isinstance(self.action, types.MessageActionChatJoinedByLink): + elif isinstance(self.action, _tl.MessageActionChatJoinedByLink): self._action_entities = [entities.get(self.action.inviter_id)] - elif isinstance(self.action, types.MessageActionChatMigrateTo): + elif isinstance(self.action, _tl.MessageActionChatMigrateTo): self._action_entities = [entities.get(utils.get_peer_id( - types.PeerChannel(self.action.channel_id)))] + _tl.PeerChannel(self.action.channel_id)))] elif isinstance( - self.action, types.MessageActionChannelMigrateFrom): + self.action, _tl.MessageActionChannelMigrateFrom): self._action_entities = [entities.get(utils.get_peer_id( - types.PeerChat(self.action.chat_id)))] + _tl.PeerChat(self.action.chat_id)))] if self.replies and self.replies.channel_id: self._linked_chat = entities.get(utils.get_peer_id( - types.PeerChannel(self.replies.channel_id))) + _tl.PeerChannel(self.replies.channel_id))) # endregion Initialization @@ -435,7 +435,7 @@ class Message(ChatGetter, SenderGetter, TLObject): """ if self._buttons_count is None: if isinstance(self.reply_markup, ( - types.ReplyInlineMarkup, types.ReplyKeyboardMarkup)): + _tl.ReplyInlineMarkup, _tl.ReplyKeyboardMarkup)): self._buttons_count = sum( len(row.buttons) for row in self.reply_markup.rows) else: @@ -471,14 +471,14 @@ class Message(ChatGetter, SenderGetter, TLObject): action is :tl:`MessageActionChatEditPhoto`, or if the message has a web preview with a photo. """ - if isinstance(self.media, types.MessageMediaPhoto): - if isinstance(self.media.photo, types.Photo): + if isinstance(self.media, _tl.MessageMediaPhoto): + if isinstance(self.media.photo, _tl.Photo): return self.media.photo - elif isinstance(self.action, types.MessageActionChatEditPhoto): + elif isinstance(self.action, _tl.MessageActionChatEditPhoto): return self.action.photo else: web = self.web_preview - if web and isinstance(web.photo, types.Photo): + if web and isinstance(web.photo, _tl.Photo): return web.photo @property @@ -486,12 +486,12 @@ class Message(ChatGetter, SenderGetter, TLObject): """ The :tl:`Document` media in this message, if any. """ - if isinstance(self.media, types.MessageMediaDocument): - if isinstance(self.media.document, types.Document): + if isinstance(self.media, _tl.MessageMediaDocument): + if isinstance(self.media.document, _tl.Document): return self.media.document else: web = self.web_preview - if web and isinstance(web.document, types.Document): + if web and isinstance(web.document, _tl.Document): return web.document @property @@ -499,8 +499,8 @@ class Message(ChatGetter, SenderGetter, TLObject): """ The :tl:`WebPage` media in this message, if any. """ - if isinstance(self.media, types.MessageMediaWebPage): - if isinstance(self.media.webpage, types.WebPage): + if isinstance(self.media, _tl.MessageMediaWebPage): + if isinstance(self.media.webpage, _tl.WebPage): return self.media.webpage @property @@ -508,7 +508,7 @@ class Message(ChatGetter, SenderGetter, TLObject): """ The :tl:`Document` media in this message, if it's an audio file. """ - return self._document_by_attribute(types.DocumentAttributeAudio, + return self._document_by_attribute(_tl.DocumentAttributeAudio, lambda attr: not attr.voice) @property @@ -516,7 +516,7 @@ class Message(ChatGetter, SenderGetter, TLObject): """ The :tl:`Document` media in this message, if it's a voice note. """ - return self._document_by_attribute(types.DocumentAttributeAudio, + return self._document_by_attribute(_tl.DocumentAttributeAudio, lambda attr: attr.voice) @property @@ -524,14 +524,14 @@ class Message(ChatGetter, SenderGetter, TLObject): """ The :tl:`Document` media in this message, if it's a video. """ - return self._document_by_attribute(types.DocumentAttributeVideo) + return self._document_by_attribute(_tl.DocumentAttributeVideo) @property def video_note(self): """ The :tl:`Document` media in this message, if it's a video note. """ - return self._document_by_attribute(types.DocumentAttributeVideo, + return self._document_by_attribute(_tl.DocumentAttributeVideo, lambda attr: attr.round_message) @property @@ -543,21 +543,21 @@ class Message(ChatGetter, SenderGetter, TLObject): sound, the so called "animated" media. However, it may be the actual gif format if the file is too large. """ - return self._document_by_attribute(types.DocumentAttributeAnimated) + return self._document_by_attribute(_tl.DocumentAttributeAnimated) @property def sticker(self): """ The :tl:`Document` media in this message, if it's a sticker. """ - return self._document_by_attribute(types.DocumentAttributeSticker) + return self._document_by_attribute(_tl.DocumentAttributeSticker) @property def contact(self): """ The :tl:`MessageMediaContact` in this message, if it's a contact. """ - if isinstance(self.media, types.MessageMediaContact): + if isinstance(self.media, _tl.MessageMediaContact): return self.media @property @@ -565,7 +565,7 @@ class Message(ChatGetter, SenderGetter, TLObject): """ The :tl:`Game` media in this message, if it's a game. """ - if isinstance(self.media, types.MessageMediaGame): + if isinstance(self.media, _tl.MessageMediaGame): return self.media.game @property @@ -573,9 +573,9 @@ class Message(ChatGetter, SenderGetter, TLObject): """ The :tl:`GeoPoint` media in this message, if it has a location. """ - if isinstance(self.media, (types.MessageMediaGeo, - types.MessageMediaGeoLive, - types.MessageMediaVenue)): + if isinstance(self.media, (_tl.MessageMediaGeo, + _tl.MessageMediaGeoLive, + _tl.MessageMediaVenue)): return self.media.geo @property @@ -583,7 +583,7 @@ class Message(ChatGetter, SenderGetter, TLObject): """ The :tl:`MessageMediaInvoice` in this message, if it's an invoice. """ - if isinstance(self.media, types.MessageMediaInvoice): + if isinstance(self.media, _tl.MessageMediaInvoice): return self.media @property @@ -591,7 +591,7 @@ class Message(ChatGetter, SenderGetter, TLObject): """ The :tl:`MessageMediaPoll` in this message, if it's a poll. """ - if isinstance(self.media, types.MessageMediaPoll): + if isinstance(self.media, _tl.MessageMediaPoll): return self.media @property @@ -599,7 +599,7 @@ class Message(ChatGetter, SenderGetter, TLObject): """ The :tl:`MessageMediaVenue` in this message, if it's a venue. """ - if isinstance(self.media, types.MessageMediaVenue): + if isinstance(self.media, _tl.MessageMediaVenue): return self.media @property @@ -607,7 +607,7 @@ class Message(ChatGetter, SenderGetter, TLObject): """ The :tl:`MessageMediaDice` in this message, if it's a dice roll. """ - if isinstance(self.media, types.MessageMediaDice): + if isinstance(self.media, _tl.MessageMediaDice): return self.media @property @@ -616,7 +616,7 @@ class Message(ChatGetter, SenderGetter, TLObject): Returns a list of entities that took part in this action. Possible cases for this are :tl:`MessageActionChatAddUser`, - :tl:`types.MessageActionChatCreate`, :tl:`MessageActionChatDeleteUser`, + :tl:`_tl.MessageActionChatCreate`, :tl:`MessageActionChatDeleteUser`, :tl:`MessageActionChatJoinedByLink` :tl:`MessageActionChatMigrateTo` and :tl:`MessageActionChannelMigrateFrom`. @@ -660,7 +660,7 @@ class Message(ChatGetter, SenderGetter, TLObject): # If the client wasn't set we can't emulate the behaviour correctly, # so as a best-effort simply return the chat peer. if self._client and not self.out and self.is_private: - return types.PeerUser(self._client._self_id) + return _tl.PeerUser(self._client._self_id) return self.peer_id @@ -722,7 +722,7 @@ class Message(ChatGetter, SenderGetter, TLObject): # However they can access them through replies... self._reply_message = await self._client.get_messages( await self.get_input_chat() if self.is_channel else None, - ids=types.InputMessageReplyTo(self.id) + ids=_tl.InputMessageReplyTo(self.id) ) if not self._reply_message: # ...unless the current message got deleted. @@ -883,7 +883,7 @@ class Message(ChatGetter, SenderGetter, TLObject): Clicks the first button or poll option for which the callable returns `True`. The callable should accept a single `MessageButton ` - or `PollAnswer ` argument. + or `PollAnswer ` argument. If you need to select multiple options in a poll, pass a list of indices to the ``i`` parameter. @@ -950,7 +950,7 @@ class Message(ChatGetter, SenderGetter, TLObject): if not chat: return None - but = types.KeyboardButtonCallback('', data) + but = _tl.KeyboardButtonCallback('', data) return await MessageButton(self._client, but, chat, None, self.id).click( share_phone=share_phone, share_geo=share_geo, password=password) @@ -1098,7 +1098,7 @@ class Message(ChatGetter, SenderGetter, TLObject): Helper methods to set the buttons given the input sender and chat. """ if self._client and isinstance(self.reply_markup, ( - types.ReplyInlineMarkup, types.ReplyKeyboardMarkup)): + _tl.ReplyInlineMarkup, _tl.ReplyKeyboardMarkup)): self._buttons = [[ MessageButton(self._client, button, chat, bot, self.id) for button in row.buttons @@ -1114,12 +1114,12 @@ class Message(ChatGetter, SenderGetter, TLObject): cannot be found but is needed. Returns `None` if it's not needed. """ if self._client and not isinstance(self.reply_markup, ( - types.ReplyInlineMarkup, types.ReplyKeyboardMarkup)): + _tl.ReplyInlineMarkup, _tl.ReplyKeyboardMarkup)): return None for row in self.reply_markup.rows: for button in row.buttons: - if isinstance(button, types.KeyboardButtonSwitchInline): + if isinstance(button, _tl.KeyboardButtonSwitchInline): # no via_bot_id means the bot sent the message itself (#1619) if button.same_peer or not self.via_bot_id: bot = self.input_sender diff --git a/telethon/_tl/custom/messagebutton.py b/telethon/_tl/custom/messagebutton.py index 61e39dc6..8596ff97 100644 --- a/telethon/_tl/custom/messagebutton.py +++ b/telethon/_tl/custom/messagebutton.py @@ -1,5 +1,5 @@ -from .. import types, functions -from ... import password as pwd_mod +from ..._misc import password as pwd_mod +from ... import _tl from ...errors import BotResponseTimeoutError import webbrowser import os @@ -46,19 +46,19 @@ class MessageButton: @property def data(self): """The `bytes` data for :tl:`KeyboardButtonCallback` objects.""" - if isinstance(self.button, types.KeyboardButtonCallback): + if isinstance(self.button, _tl.KeyboardButtonCallback): return self.button.data @property def inline_query(self): """The query `str` for :tl:`KeyboardButtonSwitchInline` objects.""" - if isinstance(self.button, types.KeyboardButtonSwitchInline): + if isinstance(self.button, _tl.KeyboardButtonSwitchInline): return self.button.query @property def url(self): """The url `str` for :tl:`KeyboardButtonUrl` objects.""" - if isinstance(self.button, types.KeyboardButtonUrl): + if isinstance(self.button, _tl.KeyboardButtonUrl): return self.button.url async def click(self, share_phone=None, share_geo=None, *, password=None): @@ -91,10 +91,10 @@ class MessageButton: this value a lot quickly may not work as expected. You may also pass a :tl:`InputGeoPoint` if you find the order confusing. """ - if isinstance(self.button, types.KeyboardButton): + if isinstance(self.button, _tl.KeyboardButton): return await self._client.send_message( self._chat, self.button.text, parse_mode=None) - elif isinstance(self.button, types.KeyboardButtonCallback): + elif isinstance(self.button, _tl.KeyboardButtonCallback): if password is not None: pwd = await self._client(_tl.fn.account.GetPassword()) password = pwd_mod.compute_check(pwd, password) @@ -107,13 +107,13 @@ class MessageButton: return await self._client(req) except BotResponseTimeoutError: return None - elif isinstance(self.button, types.KeyboardButtonSwitchInline): + elif isinstance(self.button, _tl.KeyboardButtonSwitchInline): return await self._client(_tl.fn.messages.StartBot( bot=self._bot, peer=self._chat, start_param=self.button.query )) - elif isinstance(self.button, types.KeyboardButtonUrl): + elif isinstance(self.button, _tl.KeyboardButtonUrl): return webbrowser.open(self.button.url) - elif isinstance(self.button, types.KeyboardButtonGame): + elif isinstance(self.button, _tl.KeyboardButtonGame): req = _tl.fn.messages.GetBotCallbackAnswer( peer=self._chat, msg_id=self._msg_id, game=True ) @@ -121,13 +121,13 @@ class MessageButton: return await self._client(req) except BotResponseTimeoutError: return None - elif isinstance(self.button, types.KeyboardButtonRequestPhone): + elif isinstance(self.button, _tl.KeyboardButtonRequestPhone): if not share_phone: raise ValueError('cannot click on phone buttons unless share_phone=True') if share_phone == True or isinstance(share_phone, str): me = await self._client.get_me() - share_phone = types.InputMediaContact( + share_phone = _tl.InputMediaContact( phone_number=me.phone if share_phone == True else share_phone, first_name=me.first_name or '', last_name=me.last_name or '', @@ -135,12 +135,12 @@ class MessageButton: ) return await self._client.send_file(self._chat, share_phone) - elif isinstance(self.button, types.KeyboardButtonRequestGeoLocation): + elif isinstance(self.button, _tl.KeyboardButtonRequestGeoLocation): if not share_geo: raise ValueError('cannot click on geo buttons unless share_geo=(longitude, latitude)') if isinstance(share_geo, (tuple, list)): long, lat = share_geo - share_geo = types.InputMediaGeoPoint(types.InputGeoPoint(lat=lat, long=long)) + share_geo = _tl.InputMediaGeoPoint(_tl.InputGeoPoint(lat=lat, long=long)) return await self._client.send_file(self._chat, share_geo) diff --git a/telethon/_tl/custom/participantpermissions.py b/telethon/_tl/custom/participantpermissions.py index d3719778..6d4db912 100644 --- a/telethon/_tl/custom/participantpermissions.py +++ b/telethon/_tl/custom/participantpermissions.py @@ -1,4 +1,4 @@ -from .. import types +from ... import _tl def _admin_prop(field_name, doc): @@ -85,7 +85,7 @@ class ParticipantPermissions: Whether the user left the chat. """ return isinstance(self.participant, types.ChannelParticipantLeft) - + @property def add_admins(self): """ @@ -132,7 +132,7 @@ class ParticipantPermissions: anonymous = property(**_admin_prop('anonymous', """ Whether the administrator will remain anonymous when sending messages. """)) - + manage_call = property(**_admin_prop('manage_call', """ Whether the user will be able to manage group calls. """)) diff --git a/telethon/_tl/custom/qrlogin.py b/telethon/_tl/custom/qrlogin.py index 38105921..3f2a0207 100644 --- a/telethon/_tl/custom/qrlogin.py +++ b/telethon/_tl/custom/qrlogin.py @@ -2,8 +2,7 @@ import asyncio import base64 import datetime -from .. import types, functions -from ... import events +from ... import events, _tl class QRLogin: diff --git a/telethon/_tl/patched/__init__.py b/telethon/_tl/patched/__init__.py index 2951f2af..ddffeb4c 100644 --- a/telethon/_tl/patched/__init__.py +++ b/telethon/_tl/patched/__init__.py @@ -1,4 +1,4 @@ -from .. import types, alltlobjects +from .. import _tl from ..custom.message import Message as _Message class MessageEmpty(_Message, types.MessageEmpty): diff --git a/telethon/events/album.py b/telethon/events/album.py index e06be9ee..0317db61 100644 --- a/telethon/events/album.py +++ b/telethon/events/album.py @@ -3,7 +3,9 @@ import time import weakref from .common import EventBuilder, EventCommon, name_inner_event -from .. import utils, _tl +from .._misc import utils +from .. import _tl +from .._tl import custom _IGNORE_MAX_SIZE = 100 # len() _IGNORE_MAX_AGE = 5 # seconds @@ -138,7 +140,7 @@ class Album(EventBuilder): if len(event.messages) > 1: return super().filter(event) - class Event(EventCommon, _tl.custom.sendergetter.SenderGetter): + class Event(EventCommon, custom.sendergetter.SenderGetter): """ Represents the event of a new album. @@ -158,7 +160,7 @@ class Album(EventBuilder): super().__init__(chat_peer=chat_peer, msg_id=message.id, broadcast=bool(message.post)) - _tl.custom.sendergetter.SenderGetter.__init__(self, message.sender_id) + custom.sendergetter.SenderGetter.__init__(self, message.sender_id) self.messages = messages def _set_client(self, client): diff --git a/telethon/events/callbackquery.py b/telethon/events/callbackquery.py index 954ccf2d..f850ecd5 100644 --- a/telethon/events/callbackquery.py +++ b/telethon/events/callbackquery.py @@ -2,7 +2,9 @@ import re import struct from .common import EventBuilder, EventCommon, name_inner_event -from .. import utils, _tl +from .._misc import utils +from .. import _tl +from .._tl import custom @name_inner_event @@ -121,7 +123,7 @@ class CallbackQuery(EventBuilder): return self.func(event) return True - class Event(EventCommon, _tl.custom.sendergetter.SenderGetter): + class Event(EventCommon, custom.sendergetter.SenderGetter): """ Represents the event of a new callback query. @@ -139,7 +141,7 @@ class CallbackQuery(EventBuilder): """ def __init__(self, query, peer, msg_id): super().__init__(peer, msg_id=msg_id) - _tl.custom.sendergetter.SenderGetter.__init__(self, query.user_id) + custom.sendergetter.SenderGetter.__init__(self, query.user_id) self.query = query self.data_match = None self.pattern_match = None diff --git a/telethon/events/chataction.py b/telethon/events/chataction.py index 09e1de70..d330656a 100644 --- a/telethon/events/chataction.py +++ b/telethon/events/chataction.py @@ -1,5 +1,6 @@ from .common import EventBuilder, EventCommon, name_inner_event -from .. import utils, _tl +from .._misc import utils +from .. import _tl @name_inner_event diff --git a/telethon/events/common.py b/telethon/events/common.py index 8367ce94..cce243e6 100644 --- a/telethon/events/common.py +++ b/telethon/events/common.py @@ -2,7 +2,8 @@ import abc import asyncio import warnings -from .. import utils, _tl +from .._misc import utils +from .._tl.custom.chatgetter import ChatGetter async def _into_id_set(client, chats): diff --git a/telethon/events/inlinequery.py b/telethon/events/inlinequery.py index dc602af9..f2c13d3d 100644 --- a/telethon/events/inlinequery.py +++ b/telethon/events/inlinequery.py @@ -4,7 +4,9 @@ import re import asyncio from .common import EventBuilder, EventCommon, name_inner_event -from .. import utils, _tl +from .._misc import utils +from .. import _tl +from .._tl import custom @name_inner_event @@ -72,7 +74,7 @@ class InlineQuery(EventBuilder): return super().filter(event) - class Event(EventCommon, _tl.custom.sendergetter.SenderGetter): + class Event(EventCommon, custom.sendergetter.SenderGetter): """ Represents the event of a new callback query. @@ -89,7 +91,7 @@ class InlineQuery(EventBuilder): """ def __init__(self, query): super().__init__(chat_peer=_tl.PeerUser(query.user_id)) - _tl.custom.sendergetter.SenderGetter.__init__(self, query.user_id) + custom.sendergetter.SenderGetter.__init__(self, query.user_id) self.query = query self.pattern_match = None self._answered = False diff --git a/telethon/events/messageread.py b/telethon/events/messageread.py index a25e5f66..5c37eb2c 100644 --- a/telethon/events/messageread.py +++ b/telethon/events/messageread.py @@ -1,5 +1,6 @@ from .common import EventBuilder, EventCommon, name_inner_event -from .. import utils, _tl +from .._misc import utils +from .. import _tl @name_inner_event diff --git a/telethon/events/newmessage.py b/telethon/events/newmessage.py index 192f9937..cfe7b88a 100644 --- a/telethon/events/newmessage.py +++ b/telethon/events/newmessage.py @@ -1,7 +1,8 @@ import re from .common import EventBuilder, EventCommon, name_inner_event, _into_id_set -from .. import utils, _tl +from .._misc import utils +from .. import _tl @name_inner_event diff --git a/telethon/events/raw.py b/telethon/events/raw.py index 84910778..68fdfc0c 100644 --- a/telethon/events/raw.py +++ b/telethon/events/raw.py @@ -1,5 +1,5 @@ from .common import EventBuilder -from .. import utils +from .._misc import utils class Raw(EventBuilder): diff --git a/telethon/events/userupdate.py b/telethon/events/userupdate.py index 8b6642a8..8144cadb 100644 --- a/telethon/events/userupdate.py +++ b/telethon/events/userupdate.py @@ -2,7 +2,9 @@ import datetime import functools from .common import EventBuilder, EventCommon, name_inner_event -from .. import utils, _tl +from .._misc import utils +from .. import _tl +from .._tl import custom # TODO Either the properties are poorly named or they should be @@ -63,7 +65,7 @@ class UserUpdate(EventBuilder): return cls.Event(update.user_id, typing=update.action) - class Event(EventCommon, _tl.custom.sendergetter.SenderGetter): + class Event(EventCommon, custom.sendergetter.SenderGetter): """ Represents the event of a user update such as gone online, started typing, etc. @@ -85,7 +87,7 @@ class UserUpdate(EventBuilder): """ def __init__(self, peer, *, status=None, chat_peer=None, typing=None): super().__init__(chat_peer or peer) - _tl.custom.sendergetter.SenderGetter.__init__(self, utils.get_peer_id(peer)) + custom.sendergetter.SenderGetter.__init__(self, utils.get_peer_id(peer)) self.status = status self.action = typing diff --git a/telethon/sessions/memory.py b/telethon/sessions/memory.py index 0fda05ba..9f5314a3 100644 --- a/telethon/sessions/memory.py +++ b/telethon/sessions/memory.py @@ -1,7 +1,8 @@ from enum import Enum from .abstract import Session -from .. import utils, _tl +from .._misc import utils +from .. import _tl class _SentFileType(Enum): diff --git a/telethon/sessions/sqlite.py b/telethon/sessions/sqlite.py index 32336000..5b4505c8 100644 --- a/telethon/sessions/sqlite.py +++ b/telethon/sessions/sqlite.py @@ -3,8 +3,9 @@ import os import time from .memory import MemorySession, _SentFileType -from .. import utils, _tl -from ..crypto import AuthKey +from .._misc import utils +from .. import _tl +from .._crypto import AuthKey try: import sqlite3 diff --git a/telethon/sessions/string.py b/telethon/sessions/string.py index fb971d82..72617f24 100644 --- a/telethon/sessions/string.py +++ b/telethon/sessions/string.py @@ -4,7 +4,7 @@ import struct from .abstract import Session from .memory import MemorySession -from ..crypto import AuthKey +from .._crypto import AuthKey _STRUCT_PREFORMAT = '>B{}sH256s' diff --git a/telethon_generator/generators/docs.py b/telethon_generator/generators/docs.py index 8b46e4d1..d2da55d1 100755 --- a/telethon_generator/generators/docs.py +++ b/telethon_generator/generators/docs.py @@ -9,7 +9,7 @@ from pathlib import Path from ..docswriter import DocsWriter from ..parsers import TLObject, Usability -from ..utils import snake_to_camel_case +from .._misc.utils import snake_to_camel_case CORE_TYPES = { 'int', 'long', 'int128', 'int256', 'double', diff --git a/telethon_generator/generators/tlobject.py b/telethon_generator/generators/tlobject.py index cc37cb92..04003a6b 100644 --- a/telethon_generator/generators/tlobject.py +++ b/telethon_generator/generators/tlobject.py @@ -7,7 +7,7 @@ from collections import defaultdict from zlib import crc32 from ..sourcebuilder import SourceBuilder -from ..utils import snake_to_camel_case +from .._misc.utils import snake_to_camel_case AUTO_GEN_NOTICE = \ '"""File generated by TLObjects\' generator. All changes will be ERASED"""' diff --git a/telethon_generator/parsers/errors.py b/telethon_generator/parsers/errors.py index 04cd3412..9bac2142 100644 --- a/telethon_generator/parsers/errors.py +++ b/telethon_generator/parsers/errors.py @@ -1,7 +1,7 @@ import csv import re -from ..utils import snake_to_camel_case +from .._misc.utils import snake_to_camel_case # Core base classes depending on the integer error code KNOWN_BASE_CLASSES = { diff --git a/telethon_generator/parsers/tlobject/tlobject.py b/telethon_generator/parsers/tlobject/tlobject.py index 60b9e996..0f753fa2 100644 --- a/telethon_generator/parsers/tlobject/tlobject.py +++ b/telethon_generator/parsers/tlobject/tlobject.py @@ -2,7 +2,7 @@ import re import struct import zlib -from ...utils import snake_to_camel_case +from ..._misc.utils import snake_to_camel_case # https://github.com/telegramdesktop/tdesktop/blob/4bf66cb6e93f3965b40084771b595e93d0b11bcd/Telegram/SourceFiles/codegen/scheme/codegen_scheme.py#L57-L62 WHITELISTED_MISMATCHING_IDS = { diff --git a/tests/telethon/tl/test_serialization.py b/tests/telethon/tl/test_serialization.py index 4b455784..7bcdf25b 100644 --- a/tests/telethon/tl/test_serialization.py +++ b/tests/telethon/tl/test_serialization.py @@ -1,13 +1,13 @@ import pytest -from telethon.tl import types, functions +from telethon import _tl def test_nested_invalid_serialization(): large_long = 2**62 request = _tl.fn.account.SetPrivacy( - key=types.InputPrivacyKeyChatInvite(), - rules=[types.InputPrivacyValueDisallowUsers(users=[large_long])] + key=_tl.InputPrivacyKeyChatInvite(), + rules=[_tl.InputPrivacyValueDisallowUsers(users=[large_long])] ) with pytest.raises(TypeError): bytes(request) From c84043cf71bbe200823aa1af99ec0659dcc7bc6c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 12 Sep 2021 14:09:53 +0200 Subject: [PATCH 043/256] Fix calls to private client methods --- telethon/_client/auth.py | 11 ++--- telethon/_client/downloads.py | 56 ++++++++++++------------ telethon/_client/messageparse.py | 4 +- telethon/_client/messages.py | 6 +-- telethon/_client/telegrambaseclient.py | 33 ++++---------- telethon/_client/telegramclient.py | 59 +++++++++++++++++++++++++- telethon/_client/updates.py | 18 ++++---- telethon/_client/uploads.py | 16 +++---- telethon/_client/users.py | 4 +- 9 files changed, 124 insertions(+), 83 deletions(-) diff --git a/telethon/_client/auth.py b/telethon/_client/auth.py index ccfaa68b..4ae32c43 100644 --- a/telethon/_client/auth.py +++ b/telethon/_client/auth.py @@ -40,7 +40,8 @@ async def start( raise ValueError('Both a phone and a bot token provided, ' 'must only provide one of either') - return await self._start( + return await _start( + self=self, phone=phone, password=password, bot_token=bot_token, @@ -211,7 +212,7 @@ async def sign_in( return await self.send_code_request(phone) elif code: phone, phone_code_hash = \ - self._parse_phone_and_hash(phone, phone_code_hash) + _parse_phone_and_hash(self, phone, phone_code_hash) # May raise PhoneCodeEmptyError, PhoneCodeExpiredError, # PhoneCodeHashEmptyError or PhoneCodeInvalidError. @@ -240,7 +241,7 @@ async def sign_in( self._tos = result.terms_of_service raise errors.PhoneNumberUnoccupiedError(request=request) - return self._on_login(result.user) + return _on_login(self, result.user) async def sign_up( self: 'TelegramClient', @@ -280,7 +281,7 @@ async def sign_up( sys.stderr.flush() phone, phone_code_hash = \ - self._parse_phone_and_hash(phone, phone_code_hash) + _parse_phone_and_hash(self, phone, phone_code_hash) result = await self(_tl.fn.auth.SignUp( phone_number=phone, @@ -293,7 +294,7 @@ async def sign_up( await self( _tl.fn.help.AcceptTermsOfService(self._tos.id)) - return self._on_login(result.user) + return _on_login(self, result.user) def _on_login(self, user): """ diff --git a/telethon/_client/downloads.py b/telethon/_client/downloads.py index dbb279f3..25a461cb 100644 --- a/telethon/_client/downloads.py +++ b/telethon/_client/downloads.py @@ -213,8 +213,8 @@ async def download_profile_photo( if not hasattr(entity, 'chat_photo'): return None - return await self._download_photo( - entity.chat_photo, file, date=None, + return await _download_photo( + self, entity.chat_photo, file, date=None, thumb=thumb, progress_callback=None ) @@ -237,7 +237,7 @@ async def download_profile_photo( # media which should be done with `download_media` instead. return None - file = self._get_proper_filename( + file = _get_proper_filename( file, 'profile_photo', '.jpg', possible_names=possible_names ) @@ -252,8 +252,8 @@ async def download_profile_photo( ty = helpers._entity_type(ie) if ty == helpers._EntityType.CHANNEL: full = await self(_tl.fn.channels.GetFullChannel(ie)) - return await self._download_photo( - full.full_chat.chat_photo, file, + return await _download_photo( + self, full.full_chat.chat_photo, file, date=None, progress_callback=None, thumb=thumb ) @@ -295,20 +295,20 @@ async def download_media( media = media.webpage.document or media.webpage.photo if isinstance(media, (_tl.MessageMediaPhoto, _tl.Photo)): - return await self._download_photo( - media, file, date, thumb, progress_callback + return await _download_photo( + self, media, file, date, thumb, progress_callback ) elif isinstance(media, (_tl.MessageMediaDocument, _tl.Document)): - return await self._download_document( - media, file, date, thumb, progress_callback, msg_data + return await _download_document( + self, media, file, date, thumb, progress_callback, msg_data ) elif isinstance(media, _tl.MessageMediaContact) and thumb is None: - return self._download_contact( - media, file + return _download_contact( + self, media, file ) elif isinstance(media, (_tl.WebDocument, _tl.WebDocumentNoProxy)) and thumb is None: - return await self._download_web_document( - media, file, progress_callback + return await _download_web_document( + self, media, file, progress_callback ) async def download_file( @@ -322,7 +322,8 @@ async def download_file( dc_id: int = None, key: bytes = None, iv: bytes = None) -> typing.Optional[bytes]: - return await self._download_file( + return await _download_file( + self, input_location, file, part_size_kb=part_size_kb, @@ -370,8 +371,8 @@ async def _download_file( f = file try: - async for chunk in self._iter_download( - input_location, request_size=part_size, dc_id=dc_id, msg_data=msg_data): + async for chunk in _iter_download( + self, input_location, request_size=part_size, dc_id=dc_id, msg_data=msg_data): if iv and key: chunk = AES.decrypt_ige(chunk, key, iv) r = f.write(chunk) @@ -405,7 +406,8 @@ def iter_download( file_size: int = None, dc_id: int = None ): - return self._iter_download( + return _iter_download( + self, file, offset=offset, stride=stride, @@ -552,17 +554,17 @@ async def _download_photo(self: 'TelegramClient', photo, file, date, thumb, prog return # Include video sizes here (but they may be None so provide an empty list) - size = self._get_thumb(photo.sizes + (photo.video_sizes or []), thumb) + size = _get_thumb(photo.sizes + (photo.video_sizes or []), thumb) if not size or isinstance(size, _tl.PhotoSizeEmpty): return if isinstance(size, _tl.VideoSize): - file = self._get_proper_filename(file, 'video', '.mp4', date=date) + file = _get_proper_filename(file, 'video', '.mp4', date=date) else: - file = self._get_proper_filename(file, 'photo', '.jpg', date=date) + file = _get_proper_filename(file, 'photo', '.jpg', date=date) if isinstance(size, (_tl.PhotoCachedSize, _tl.PhotoStrippedSize)): - return self._download_cached_photo_size(size, file) + return _download_cached_photo_size(self, size, file) if isinstance(size, _tl.PhotoSizeProgressive): file_size = max(size.sizes) @@ -614,19 +616,19 @@ async def _download_document( return if thumb is None: - kind, possible_names = self._get_kind_and_names(document.attributes) - file = self._get_proper_filename( + kind, possible_names = _get_kind_and_names(document.attributes) + file = _get_proper_filename( file, kind, utils.get_extension(document), date=date, possible_names=possible_names ) size = None else: - file = self._get_proper_filename(file, 'photo', '.jpg', date=date) - size = self._get_thumb(document.thumbs, thumb) + file = _get_proper_filename(file, 'photo', '.jpg', date=date) + size = _get_thumb(document.thumbs, thumb) if isinstance(size, (_tl.PhotoCachedSize, _tl.PhotoStrippedSize)): - return self._download_cached_photo_size(size, file) + return _download_cached_photo_size(self, size, file) - result = await self._download_file( + result = await _download_file( _tl.InputDocumentFileLocation( id=document.id, access_hash=document.access_hash, diff --git a/telethon/_client/messageparse.py b/telethon/_client/messageparse.py index d68ab38f..9f1f3b70 100644 --- a/telethon/_client/messageparse.py +++ b/telethon/_client/messageparse.py @@ -55,12 +55,12 @@ async def _parse_message_text(self: 'TelegramClient', message, parse_mode): 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) + is_mention = await _replace_with_mention(self, msg_entities, i, user) if not is_mention: del msg_entities[i] elif isinstance(e, (_tl.MessageEntityMentionName, _tl.InputMessageEntityMentionName)): - is_mention = await self._replace_with_mention(msg_entities, i, e.user_id) + is_mention = await _replace_with_mention(self, msg_entities, i, e.user_id) if not is_mention: del msg_entities[i] diff --git a/telethon/_client/messages.py b/telethon/_client/messages.py index 00ee418a..5dae6eb4 100644 --- a/telethon/_client/messages.py +++ b/telethon/_client/messages.py @@ -436,7 +436,7 @@ async def send_message( entity = await self.get_input_entity(entity) if comment_to is not None: - entity, reply_to = await self._get_comment_data(entity, comment_to) + entity, reply_to = await _get_comment_data(self, entity, comment_to) if isinstance(message, _tl.Message): if buttons is None: @@ -712,7 +712,7 @@ async def pin_message( notify: bool = False, pm_oneside: bool = False ): - return await self._pin(entity, message, unpin=False, notify=notify, pm_oneside=pm_oneside) + return await _pin(self, entity, message, unpin=False, notify=notify, pm_oneside=pm_oneside) async def unpin_message( self: 'TelegramClient', @@ -721,7 +721,7 @@ async def unpin_message( *, notify: bool = False ): - return await self._pin(entity, message, unpin=True, notify=notify) + return await _pin(self, entity, message, unpin=True, notify=notify) async def _pin(self, entity, message, *, unpin, notify=False, pm_oneside=False): message = utils.get_message_id(message) or 0 diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index c89b1809..1cf74821 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -331,7 +331,7 @@ def is_connected(self: 'TelegramClient') -> bool: return sender and sender.is_connected() async def disconnect(self: 'TelegramClient'): - return await self._disconnect_coro() + return await _disconnect_coro(self) def set_proxy(self: 'TelegramClient', proxy: typing.Union[tuple, dict]): init_proxy = None if not issubclass(self._connection, TcpMTProxy) else \ @@ -354,7 +354,7 @@ def set_proxy(self: 'TelegramClient', proxy: typing.Union[tuple, dict]): connection._proxy = proxy async def _disconnect_coro(self: 'TelegramClient'): - await self._disconnect() + await _disconnect(self) # Also clean-up all exported senders because we're done with them async with self._borrow_sender_lock: @@ -408,7 +408,7 @@ async def _switch_dc(self: 'TelegramClient', new_dc): Permanently switches the current connection to the new data center. """ self._log[__name__].info('Reconnecting to new data center %s', new_dc) - dc = await self._get_dc(new_dc) + dc = await _get_dc(self, new_dc) self.session.set_dc(dc.id, dc.ip_address, dc.port) # auth_key's are associated with a server, which has now changed @@ -416,7 +416,7 @@ async def _switch_dc(self: 'TelegramClient', new_dc): self._sender.auth_key.key = None self.session.auth_key = None self.session.save() - await self._disconnect() + await _disconnect(self) return await self.connect() def _auth_key_callback(self: 'TelegramClient', auth_key): @@ -462,7 +462,7 @@ async def _create_exported_sender(self: 'TelegramClient', dc_id): """ # Thanks badoualy/kotlogram on /telegram/api/DefaultTelegramClient.kt # for clearly showing how to export the authorization - dc = await self._get_dc(dc_id) + dc = await _get_dc(self, dc_id) # Can't reuse self._sender._connection as it has its own seqno. # # If one were to do that, Telegram would reset the connection @@ -497,12 +497,12 @@ async def _borrow_exported_sender(self: 'TelegramClient', dc_id): if state is None: state = _ExportState() - sender = await self._create_exported_sender(dc_id) + sender = await _create_exported_sender(self, dc_id) sender.dc_id = dc_id self._borrowed_senders[dc_id] = (state, sender) elif state.need_connect(): - dc = await self._get_dc(dc_id) + dc = await _get_dc(self, dc_id) await sender.connect(self._connection( dc.ip_address, dc.port, @@ -545,7 +545,7 @@ async def _get_cdn_client(self: 'TelegramClient', cdn_redirect): raise NotImplementedError session = self._exported_sessions.get(cdn_redirect.dc_id) if not session: - dc = await self._get_dc(cdn_redirect.dc_id, cdn=True) + dc = await _get_dc(self, cdn_redirect.dc_id, cdn=True) session = self.session.clone() await session.set_dc(dc.id, dc.ip_address, dc.port) self._exported_sessions[cdn_redirect.dc_id] = session @@ -564,20 +564,3 @@ async def _get_cdn_client(self: 'TelegramClient', cdn_redirect): # set already. Avoid invoking non-CDN methods by not syncing updates. client.connect(_sync_updates=False) return client - - -@abc.abstractmethod -def __call__(self: 'TelegramClient', request, ordered=False): - raise NotImplementedError - -@abc.abstractmethod -def _handle_update(self: 'TelegramClient', update): - raise NotImplementedError - -@abc.abstractmethod -def _update_loop(self: 'TelegramClient'): - raise NotImplementedError - -@abc.abstractmethod -async def _handle_auto_reconnect(self: 'TelegramClient'): - raise NotImplementedError diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 986b89f0..f219605e 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -3496,7 +3496,7 @@ class TelegramClient: # region Users - def __call__(self: 'TelegramClient', request, ordered=False): + async def __call__(self: 'TelegramClient', request, ordered=False, flood_sleep_threshold=None): """ Invokes (sends) one or more MTProtoRequests and returns (receives) their result. @@ -3519,7 +3519,7 @@ class TelegramClient: The result of the request (often a `TLObject`) or a list of results if more than one request was given. """ - return users.call(self._sender, request, ordered=ordered) + return self._call(request, ordered, flood_sleep_threshold) async def get_me(self: 'TelegramClient', input_peer: bool = False) \ -> 'typing.Union[_tl.User, _tl.InputPeerUser]': @@ -3719,4 +3719,59 @@ class TelegramClient: # endregion Users + # region Private + + async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sleep_threshold=None): + return users.call(self._sender, request, ordered=ordered, flood_sleep_threshold=flood_sleep_threshold) + + async def _update_loop(self: 'TelegramClient'): + return updates._update_loop(**locals()) + + async def _parse_message_text(self: 'TelegramClient', message, parse_mode): + return messageparse._parse_message_text(**locals()) + + async def _file_to_media( + self, file, force_document=False, file_size=None, + progress_callback=None, attributes=None, thumb=None, + allow_cache=True, voice_note=False, video_note=False, + supports_streaming=False, mime_type=None, as_image=None, + ttl=None): + return uploads._file_to_media(**locals()) + + async def _get_peer(self: 'TelegramClient', peer: 'hints.EntityLike'): + return users._get_peer(**locals()) + + def _get_response_message(self: 'TelegramClient', request, result, input_chat): + return messageparse._get_response_message(**locals()) + + async def _get_comment_data( + self: 'TelegramClient', + entity: 'hints.EntityLike', + message: 'typing.Union[int, _tl.Message]' + ): + return messages._get_comment_data(**locals()) + + async def _switch_dc(self: 'TelegramClient', new_dc): + return telegrambaseclient._switch_dc(**locals()) + + async def _borrow_exported_sender(self: 'TelegramClient', dc_id): + return telegrambaseclient._borrow_exported_sender(**locals()) + + async def _return_exported_sender(self: 'TelegramClient', sender): + return telegrambaseclient._return_exported_sender(**locals()) + + async def _clean_exported_senders(self: 'TelegramClient'): + return telegrambaseclient._clean_exported_senders(**locals()) + + def _auth_key_callback(self: 'TelegramClient', auth_key): + return telegrambaseclient._auth_key_callback + + def _handle_update(self: 'TelegramClient', update): + return updates._handle_update(**locals()) + + async def _handle_auto_reconnect(self: 'TelegramClient'): + return updates._handle_auto_reconnect(**locals()) + + # endregion Private + # TODO re-patch everything to remove the intermediate calls diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index e8c5709c..9d1e205c 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -34,7 +34,7 @@ async def set_receive_updates(self: 'TelegramClient', receive_updates): await self(_tl.fn.updates.GetState()) async def run_until_disconnected(self: 'TelegramClient'): - return await self._run_until_disconnected() + return await _run_until_disconnected(self) def on(self: 'TelegramClient', event: EventBuilder): def decorator(f): @@ -101,7 +101,7 @@ async def catch_up(self: 'TelegramClient'): state = d.intermediate_state pts, date = state.pts, state.date - self._handle_update(_tl.Updates( + _handle_update(self, _tl.Updates( users=d.users, chats=d.chats, date=state.date, @@ -151,11 +151,11 @@ def _handle_update(self: 'TelegramClient', update): entities = {utils.get_peer_id(x): x for x in itertools.chain(update.users, update.chats)} for u in update.updates: - self._process_update(u, update.updates, entities=entities) + _process_update(self, u, update.updates, entities=entities) elif isinstance(update, _tl.UpdateShort): - self._process_update(update.update, None) + _process_update(self, update.update, None) else: - self._process_update(update, None) + _process_update(self, update, None) self._state_cache.update(update) @@ -168,14 +168,14 @@ def _process_update(self: 'TelegramClient', update, others, entities=None): channel_id = self._state_cache.get_channel_id(update) args = (update, others, channel_id, self._state_cache[channel_id]) if self._dispatching_updates_queue is None: - task = self.loop.create_task(self._dispatch_update(*args)) + task = self.loop.create_task(_dispatch_update(self, *args)) self._updates_queue.add(task) task.add_done_callback(lambda _: self._updates_queue.discard(task)) else: self._updates_queue.put_nowait(args) if not self._dispatching_updates_queue.is_set(): self._dispatching_updates_queue.set() - self.loop.create_task(self._dispatch_queue_updates()) + self.loop.create_task(_dispatch_queue_updates(self)) self._state_cache.update(update) @@ -235,7 +235,7 @@ async def _update_loop(self: 'TelegramClient'): async def _dispatch_queue_updates(self: 'TelegramClient'): while not self._updates_queue.empty(): - await self._dispatch_update(*self._updates_queue.get_nowait()) + await _dispatch_update(self, *self._updates_queue.get_nowait()) self._dispatching_updates_queue.clear() @@ -248,7 +248,7 @@ async def _dispatch_update(self: 'TelegramClient', update, others, channel_id, p # If the update doesn't have pts, fetching won't do anything. # For example, UpdateUserStatus or UpdateChatUserTyping. try: - await self._get_difference(update, channel_id, pts_date) + await _get_difference(self, update, channel_id, pts_date) except OSError: pass # We were disconnected, that's okay except errors.RPCError: diff --git a/telethon/_client/uploads.py b/telethon/_client/uploads.py index db2cdd77..8043f26c 100644 --- a/telethon/_client/uploads.py +++ b/telethon/_client/uploads.py @@ -125,7 +125,7 @@ async def send_file( entity = await self.get_input_entity(entity) if comment_to is not None: - entity, reply_to = await self._get_comment_data(entity, comment_to) + entity, reply_to = await _get_comment_data(self, entity, comment_to) else: reply_to = utils.get_message_id(reply_to) @@ -139,8 +139,8 @@ async def send_file( result = [] while file: - result += await self._send_album( - entity, file[:10], caption=captions[:10], + result += await _send_album( + self, entity, file[:10], caption=captions[:10], progress_callback=progress_callback, reply_to=reply_to, parse_mode=parse_mode, silent=silent, schedule=schedule, supports_streaming=supports_streaming, clear_draft=clear_draft, @@ -167,10 +167,10 @@ async def send_file( msg_entities = formatting_entities else: caption, msg_entities =\ - await self._parse_message_text(caption, parse_mode) + await _parse_message_text(self, caption, parse_mode) - file_handle, media, image = await self._file_to_media( - file, force_document=force_document, + file_handle, media, image = await _file_to_media( + self, file, force_document=force_document, file_size=file_size, progress_callback=progress_callback, attributes=attributes, allow_cache=allow_cache, thumb=thumb, @@ -223,8 +223,8 @@ async def _send_album(self: 'TelegramClient', entity, files, caption='', # :tl:`InputMediaUploadedPhoto`. However using that will # make it `raise MediaInvalidError`, so we need to upload # it as media and then convert that to :tl:`InputMediaPhoto`. - fh, fm, _ = await self._file_to_media( - file, supports_streaming=supports_streaming, + fh, fm, _ = await _file_to_media( + self, file, supports_streaming=supports_streaming, force_document=force_document, ttl=ttl) if isinstance(fm, (_tl.InputMediaUploadedPhoto, _tl.InputMediaPhotoExternal)): r = await self(_tl.fn.messages.UploadMedia( diff --git a/telethon/_client/users.py b/telethon/_client/users.py index e493ea61..0719cdc4 100644 --- a/telethon/_client/users.py +++ b/telethon/_client/users.py @@ -230,7 +230,7 @@ async def get_entity( result = [] for x in inputs: if isinstance(x, str): - result.append(await self._get_entity_from_string(x)) + result.append(await _get_entity_from_string(self, x)) elif not isinstance(x, _tl.InputPeerSelf): result.append(id_entity[utils.get_peer_id(x)]) else: @@ -271,7 +271,7 @@ async def get_input_entity( # Only network left to try if isinstance(peer, str): return utils.get_input_peer( - await self._get_entity_from_string(peer)) + await _get_entity_from_string(self, peer)) # If we're a bot and the user has messaged us privately users.getUsers # will work with access_hash = 0. Similar for channels.getChannels. From e9b97b5e4a746a2d181ee041049927549f4d46c2 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 12 Sep 2021 15:46:57 +0200 Subject: [PATCH 044/256] Fix client method calls and reading TLObjects --- telethon/_client/telegrambaseclient.py | 3 +- telethon/_client/telegramclient.py | 125 ++++++++++++++----------- telethon/_client/uploads.py | 2 +- telethon/_client/users.py | 8 +- telethon/_misc/binaryreader.py | 6 +- 5 files changed, 81 insertions(+), 63 deletions(-) diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 1cf74821..b45ac6d3 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -11,6 +11,7 @@ from .. import version, helpers, __name__ as __base_name__, _tl from .._crypto import rsa from .._misc import markdown, entitycache, statecache from .._network import MTProtoSender, Connection, ConnectionTcpFull, TcpMTProxy +from .._tl.alltlobjects import LAYER from ..sessions import Session, SQLiteSession, MemorySession DEFAULT_DC_ID = 2 @@ -321,7 +322,7 @@ async def connect(self: 'TelegramClient') -> None: self._init_request.query = _tl.fn.help.GetConfig() await self._sender.send(_tl.fn.InvokeWithLayer( - _tl.alltlobjects.LAYER, self._init_request + LAYER, self._init_request )) self._updates_handle = self.loop.create_task(self._update_loop()) diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index f219605e..3596a506 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -274,7 +274,7 @@ class TelegramClient: # region Auth - def start( + async def start( self: 'TelegramClient', phone: typing.Callable[[], str] = lambda: input('Please enter your phone (or bot token): '), password: typing.Callable[[], str] = lambda: getpass.getpass('Please enter your password: '), @@ -360,7 +360,7 @@ class TelegramClient: with client: pass """ - return auth.start(**locals()) + return await auth.start(**locals()) async def sign_in( self: 'TelegramClient', @@ -419,7 +419,7 @@ class TelegramClient: code = input('enter code: ') await client.sign_in(phone, code) """ - return auth.sign_in(**locals()) + return await auth.sign_in(**locals()) async def sign_up( self: 'TelegramClient', @@ -471,7 +471,7 @@ class TelegramClient: code = input('enter code: ') await client.sign_up(code, first_name='Anna', last_name='Banana') """ - return auth.sign_up(**locals()) + return await auth.sign_up(**locals()) async def send_code_request( self: 'TelegramClient', @@ -498,7 +498,7 @@ class TelegramClient: sent = await client.send_code_request(phone) print(sent) """ - return auth.send_code_request(**locals()) + return await auth.send_code_request(**locals()) async def qr_login(self: 'TelegramClient', ignored_ids: typing.List[int] = None) -> custom.QRLogin: """ @@ -533,7 +533,7 @@ class TelegramClient: # Important! You need to wait for the login to complete! await qr_login.wait() """ - return auth.qr_login(**locals()) + return await auth.qr_login(**locals()) async def log_out(self: 'TelegramClient') -> bool: """ @@ -548,7 +548,7 @@ class TelegramClient: # Note: you will need to login again! await client.log_out() """ - return auth.log_out(**locals()) + return await auth.log_out(**locals()) async def edit_2fa( self: 'TelegramClient', @@ -611,7 +611,7 @@ class TelegramClient: # Removing the password await client.edit_2fa(current_password='I_<3_Telethon') """ - return auth.edit_2fa(**locals()) + return await auth.edit_2fa(**locals()) async def __aenter__(self): return await self.start() @@ -670,7 +670,7 @@ class TelegramClient: # Send the first result to some chat message = await results[0].click('TelethonOffTopic') """ - return bots.inline_query(**locals()) + return await bots.inline_query(**locals()) # endregion Bots @@ -709,8 +709,8 @@ class TelegramClient: # later await client.send_message(chat, 'click me', buttons=markup) """ - return buttons.build_reply_markup(**locals()) - + from . import buttons as b + return b.build_reply_markup(buttons=buttons, inline_only=inline_only) # endregion Buttons @@ -805,7 +805,7 @@ class TelegramClient: if user.username is not None: print(user.username) """ - return chats.get_participants(*args, **kwargs) + return await chats.get_participants(*args, **kwargs) get_participants.__signature__ = inspect.signature(iter_participants) @@ -950,7 +950,7 @@ class TelegramClient: # Print the old message before it was deleted print(events[0].old) """ - return chats.get_admin_log(*args, **kwargs) + return await chats.get_admin_log(*args, **kwargs) get_admin_log.__signature__ = inspect.signature(iter_admin_log) @@ -1011,7 +1011,7 @@ class TelegramClient: # Download the oldest photo await client.download_media(photos[-1]) """ - return chats.get_profile_photos(*args, **kwargs) + return await chats.get_profile_photos(*args, **kwargs) get_profile_photos.__signature__ = inspect.signature(iter_profile_photos) @@ -1197,7 +1197,7 @@ class TelegramClient: # Granting all permissions except for `add_admins` await client.edit_admin(chat, user, is_admin=True, add_admins=False) """ - return chats.edit_admin(**locals()) + return await chats.edit_admin(**locals()) async def edit_permissions( self: 'TelegramClient', @@ -1314,7 +1314,7 @@ class TelegramClient: await client.edit_permissions(chat, user, view_messages=False) await client.edit_permissions(chat, user) """ - return chats.edit_permissions(**locals()) + return await chats.edit_permissions(**locals()) async def kick_participant( self: 'TelegramClient', @@ -1353,7 +1353,7 @@ class TelegramClient: # Leaving chat await client.kick_participant(chat, 'me') """ - return chats.kick_participant(**locals()) + return await chats.kick_participant(**locals()) async def get_permissions( self: 'TelegramClient', @@ -1391,7 +1391,7 @@ class TelegramClient: # Get Banned Permissions of Chat await client.get_permissions(chat) """ - return chats.get_permissions(**locals()) + return await chats.get_permissions(**locals()) async def get_stats( self: 'TelegramClient', @@ -1437,7 +1437,7 @@ class TelegramClient: .. _`at least 500 members`: https://telegram.org/blog/profile-videos-people-nearby-and-more """ - return chats.get_stats(**locals()) + return await chats.get_stats(**locals()) # endregion Chats @@ -1544,7 +1544,7 @@ class TelegramClient: archived = await client.get_dialogs(folder=1) archived = await client.get_dialogs(archived=True) """ - return dialogs.get_dialogs(*args, **kwargs) + return await dialogs.get_dialogs(*args, **kwargs) get_dialogs.__signature__ = inspect.signature(iter_dialogs) @@ -1596,7 +1596,7 @@ class TelegramClient: draft = await client.get_drafts('me') print(drafts.text) """ - return dialogs.get_drafts(**locals()) + return await dialogs.get_drafts(**locals()) async def edit_folder( self: 'TelegramClient', @@ -1655,7 +1655,7 @@ class TelegramClient: # Un-archiving all dialogs await client.edit_folder(unpack=1) """ - return dialogs.edit_folder(**locals()) + return await dialogs.edit_folder(**locals()) async def delete_dialog( self: 'TelegramClient', @@ -1700,7 +1700,7 @@ class TelegramClient: # Leaving a channel by username await client.delete_dialog('username') """ - return dialogs.delete_dialog(**locals()) + return await dialogs.delete_dialog(**locals()) # endregion Dialogs @@ -1749,7 +1749,7 @@ class TelegramClient: path = await client.download_profile_photo('me') print(path) """ - return downloads.download_profile_photo(**locals()) + return await downloads.download_profile_photo(**locals()) async def download_media( self: 'TelegramClient', @@ -1825,7 +1825,7 @@ class TelegramClient: await client.download_media(message, progress_callback=callback) """ - return downloads.download_media(**locals()) + return await downloads.download_media(**locals()) async def download_file( self: 'TelegramClient', @@ -1890,7 +1890,7 @@ class TelegramClient: data = await client.download_file(input_file, bytes) print(data[:16]) """ - return downloads.download_file(**locals()) + return await downloads.download_file(**locals()) def iter_download( self: 'TelegramClient', @@ -2249,7 +2249,7 @@ class TelegramClient: # Get messages by ID: message_1337 = await client.get_messages(chat, ids=1337) """ - return messages.get_messages(**locals()) + return await messages.get_messages(**locals()) get_messages.__signature__ = inspect.signature(iter_messages) @@ -2445,7 +2445,7 @@ class TelegramClient: from datetime import timedelta await client.send_message(chat, 'Hi, future!', schedule=timedelta(minutes=5)) """ - return messages.send_message(**locals()) + return await messages.send_message(**locals()) async def forward_messages( self: 'TelegramClient', @@ -2527,7 +2527,18 @@ class TelegramClient: # Forwarding as a copy await client.send_message(chat, message) """ - return messages.forward_messages(**locals()) + from . import messages as m + return await m.forward_messages( + self=self, + entity=entity, + messages=messages, + from_peer=from_peer, + background=background, + with_my_score=with_my_score, + silent=silent, + as_album=as_album, + schedule=schedule + ) async def edit_message( self: 'TelegramClient', @@ -2656,7 +2667,7 @@ class TelegramClient: # or await client.edit_message(message, 'hello!!!') """ - return messages.edit_message(**locals()) + return await messages.edit_message(**locals()) async def delete_messages( self: 'TelegramClient', @@ -2708,7 +2719,7 @@ class TelegramClient: await client.delete_messages(chat, messages) """ - return messages.delete_messages(**locals()) + return await messages.delete_messages(**locals()) async def send_read_acknowledge( self: 'TelegramClient', @@ -2760,7 +2771,7 @@ class TelegramClient: # ...or passing a list of messages to mark as read await client.send_read_acknowledge(chat, messages) """ - return messages.send_read_acknowledge(**locals()) + return await messages.send_read_acknowledge(**locals()) async def pin_message( self: 'TelegramClient', @@ -2801,7 +2812,7 @@ class TelegramClient: message = await client.send_message(chat, 'Pinotifying is fun!') await client.pin_message(chat, message, notify=True) """ - return messages.pin_message(**locals()) + return await messages.pin_message(**locals()) async def unpin_message( self: 'TelegramClient', @@ -2831,7 +2842,7 @@ class TelegramClient: # Unpin all messages from a chat await client.unpin_message(chat) """ - return messages.unpin_message(**locals()) + return await messages.unpin_message(**locals()) # endregion Messages @@ -2938,7 +2949,7 @@ class TelegramClient: except OSError: print('Failed to connect') """ - return telegrambaseclient.connect(**locals()) + return await telegrambaseclient.connect(**locals()) def is_connected(self: 'TelegramClient') -> bool: """ @@ -2993,7 +3004,7 @@ class TelegramClient: This is an `async` method, because in order for Telegram to start sending updates again, a request must be made. """ - return updates.set_receive_updates(**locals()) + return await updates.set_receive_updates(**locals()) def run_until_disconnected(self: 'TelegramClient'): """ @@ -3154,7 +3165,7 @@ class TelegramClient: await client.catch_up() """ - return updates.catch_up(**locals()) + return await updates.catch_up(**locals()) # endregion Updates @@ -3403,7 +3414,7 @@ class TelegramClient: vcard='' )) """ - return uploads.send_file(**locals()) + return await uploads.send_file(**locals()) async def upload_file( self: 'TelegramClient', @@ -3490,7 +3501,7 @@ class TelegramClient: await client.send_file(chat, file) # sends as song await client.send_file(chat, file, voice_note=True) # sends as voice note """ - return uploads.upload_file(**locals()) + return await uploads.upload_file(**locals()) # endregion Uploads @@ -3519,7 +3530,7 @@ class TelegramClient: The result of the request (often a `TLObject`) or a list of results if more than one request was given. """ - return self._call(request, ordered, flood_sleep_threshold) + return await users.call(**locals()) async def get_me(self: 'TelegramClient', input_peer: bool = False) \ -> 'typing.Union[_tl.User, _tl.InputPeerUser]': @@ -3543,7 +3554,7 @@ class TelegramClient: me = await client.get_me() print(me.username) """ - return users.get_me(**locals()) + return await users.get_me(**locals()) async def is_bot(self: 'TelegramClient') -> bool: """ @@ -3557,7 +3568,7 @@ class TelegramClient: else: print('Hello') """ - return users.is_bot(**locals()) + return await users.is_bot(**locals()) async def is_user_authorized(self: 'TelegramClient') -> bool: """ @@ -3571,7 +3582,7 @@ class TelegramClient: code = input('enter code: ') await client.sign_in(phone, code) """ - return users.is_user_authorized(**locals()) + return await users.is_user_authorized(**locals()) async def get_entity( self: 'TelegramClient', @@ -3629,7 +3640,7 @@ class TelegramClient: # Note that for this to work the phone number must be in your contacts some_id = await client.get_peer_id('+34123456789') """ - return users.get_entity(**locals()) + return await users.get_entity(**locals()) async def get_input_entity( self: 'TelegramClient', @@ -3695,7 +3706,7 @@ class TelegramClient: # The same applies to IDs, chats or channels. chat = await client.get_input_entity(-123456789) """ - return users.get_input_entity(**locals()) + return await users.get_input_entity(**locals()) async def get_peer_id( self: 'TelegramClient', @@ -3715,20 +3726,20 @@ class TelegramClient: print(await client.get_peer_id('me')) """ - return users.get_peer_id(**locals()) + return await users.get_peer_id(**locals()) # endregion Users # region Private async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sleep_threshold=None): - return users.call(self._sender, request, ordered=ordered, flood_sleep_threshold=flood_sleep_threshold) + return await users._call(**locals()) async def _update_loop(self: 'TelegramClient'): - return updates._update_loop(**locals()) + return await updates._update_loop(**locals()) async def _parse_message_text(self: 'TelegramClient', message, parse_mode): - return messageparse._parse_message_text(**locals()) + return await messageparse._parse_message_text(**locals()) async def _file_to_media( self, file, force_document=False, file_size=None, @@ -3736,10 +3747,10 @@ class TelegramClient: allow_cache=True, voice_note=False, video_note=False, supports_streaming=False, mime_type=None, as_image=None, ttl=None): - return uploads._file_to_media(**locals()) + return await uploads._file_to_media(**locals()) async def _get_peer(self: 'TelegramClient', peer: 'hints.EntityLike'): - return users._get_peer(**locals()) + return await users._get_peer(**locals()) def _get_response_message(self: 'TelegramClient', request, result, input_chat): return messageparse._get_response_message(**locals()) @@ -3749,19 +3760,19 @@ class TelegramClient: entity: 'hints.EntityLike', message: 'typing.Union[int, _tl.Message]' ): - return messages._get_comment_data(**locals()) + return await messages._get_comment_data(**locals()) async def _switch_dc(self: 'TelegramClient', new_dc): - return telegrambaseclient._switch_dc(**locals()) + return await telegrambaseclient._switch_dc(**locals()) async def _borrow_exported_sender(self: 'TelegramClient', dc_id): - return telegrambaseclient._borrow_exported_sender(**locals()) + return await telegrambaseclient._borrow_exported_sender(**locals()) async def _return_exported_sender(self: 'TelegramClient', sender): - return telegrambaseclient._return_exported_sender(**locals()) + return await telegrambaseclient._return_exported_sender(**locals()) async def _clean_exported_senders(self: 'TelegramClient'): - return telegrambaseclient._clean_exported_senders(**locals()) + return await telegrambaseclient._clean_exported_senders(**locals()) def _auth_key_callback(self: 'TelegramClient', auth_key): return telegrambaseclient._auth_key_callback @@ -3770,7 +3781,7 @@ class TelegramClient: return updates._handle_update(**locals()) async def _handle_auto_reconnect(self: 'TelegramClient'): - return updates._handle_auto_reconnect(**locals()) + return await updates._handle_auto_reconnect(**locals()) # endregion Private diff --git a/telethon/_client/uploads.py b/telethon/_client/uploads.py index 8043f26c..c8fbcea7 100644 --- a/telethon/_client/uploads.py +++ b/telethon/_client/uploads.py @@ -167,7 +167,7 @@ async def send_file( msg_entities = formatting_entities else: caption, msg_entities =\ - await _parse_message_text(self, caption, parse_mode) + await self._parse_message_text(caption, parse_mode) file_handle, media, image = await _file_to_media( self, file, force_document=force_document, diff --git a/telethon/_client/users.py b/telethon/_client/users.py index 0719cdc4..9bf18faf 100644 --- a/telethon/_client/users.py +++ b/telethon/_client/users.py @@ -24,12 +24,16 @@ def _fmt_flood(delay, request, *, early=False, td=datetime.timedelta): ) -async def call(self: 'TelegramClient', sender, request, ordered=False, flood_sleep_threshold=None): +async def call(self: 'TelegramClient', request, ordered=False, flood_sleep_threshold=None): + return await _call(self, self._sender, request, ordered=ordered) + + +async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sleep_threshold=None): if flood_sleep_threshold is None: flood_sleep_threshold = self.flood_sleep_threshold requests = (request if utils.is_list_like(request) else (request,)) for r in requests: - if not isinstance(r, TLRequest): + if not isinstance(r, _tl.TLRequest): raise _NOT_A_REQUEST() await r.resolve(self, utils) diff --git a/telethon/_misc/binaryreader.py b/telethon/_misc/binaryreader.py index 6a87b64d..e5c34c7f 100644 --- a/telethon/_misc/binaryreader.py +++ b/telethon/_misc/binaryreader.py @@ -9,6 +9,8 @@ from struct import unpack from ..errors import TypeNotFoundError from .. import _tl +from .._tl.alltlobjects import tlobjects +from .._tl import core _EPOCH_NAIVE = datetime(*time.gmtime(0)[:6]) _EPOCH = _EPOCH_NAIVE.replace(tzinfo=timezone.utc) @@ -117,7 +119,7 @@ class BinaryReader: def tgread_object(self): """Reads a Telegram object.""" constructor_id = self.read_int(signed=False) - clazz = _tl.tlobjects.get(constructor_id, None) + clazz = tlobjects.get(constructor_id, None) if clazz is None: # The class was None, but there's still a # chance of it being a manually parsed value like bool! @@ -129,7 +131,7 @@ class BinaryReader: elif value == 0x1cb5c415: # Vector return [self.tgread_object() for _ in range(self.read_int())] - clazz = _tl.core.get(constructor_id, None) + clazz = core.core_objects.get(constructor_id, None) if clazz is None: # If there was still no luck, give up self.seek(-4) # Go back From f6c94f4d84aed53252504318c470e0725dc058d6 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 12 Sep 2021 15:50:26 +0200 Subject: [PATCH 045/256] Mention Python 3.5 will no longer be supported --- readthedocs/misc/v2-migration-guide.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 8f9bab4d..ccf53027 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -12,6 +12,12 @@ from Telethon version 1.x to 2.0 onwards. **Please read this document in full before upgrading your code to Telethon 2.0.** +Pyhton 3.5 is no longer supported +--------------------------------- + +The library will no longer attempt to support Python 3.5. The minimum version is now Python 3.6. + + User, chat and channel identifiers are now 64-bit numbers --------------------------------------------------------- @@ -96,6 +102,7 @@ This serves multiple goals: // TODO this definitely generated files mapping from the original name to this new one... + Synchronous compatibility mode has been removed ----------------------------------------------- From c08d724baaf178bb68bc4030ac6f571fff971599 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 12 Sep 2021 15:52:01 +0200 Subject: [PATCH 046/256] Delete _tl.patched backward-compatibility hack --- telethon/_tl/patched/__init__.py | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 telethon/_tl/patched/__init__.py diff --git a/telethon/_tl/patched/__init__.py b/telethon/_tl/patched/__init__.py deleted file mode 100644 index ddffeb4c..00000000 --- a/telethon/_tl/patched/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -from .. import _tl -from ..custom.message import Message as _Message - -class MessageEmpty(_Message, types.MessageEmpty): - pass - -types.MessageEmpty = MessageEmpty -alltlobjects.tlobjects[MessageEmpty.CONSTRUCTOR_ID] = MessageEmpty - -class MessageService(_Message, types.MessageService): - pass - -types.MessageService = MessageService -alltlobjects.tlobjects[MessageService.CONSTRUCTOR_ID] = MessageService - -class Message(_Message, types.Message): - pass - -types.Message = Message -alltlobjects.tlobjects[Message.CONSTRUCTOR_ID] = Message From 604c3de070a6f318347fc0ebcf101402d7726cd7 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 12 Sep 2021 16:05:56 +0200 Subject: [PATCH 047/256] Move custom and core objects to a new subpackage This should keep it cleaner, as now _tl is fully auto-generated. --- telethon/{_tl/core => types/_core}/__init__.py | 0 telethon/{_tl/core => types/_core}/gzippacked.py | 0 telethon/{_tl/core => types/_core}/messagecontainer.py | 0 telethon/{_tl/core => types/_core}/rpcresult.py | 0 telethon/{_tl/core => types/_core}/tlmessage.py | 0 telethon/{_tl/custom => types/_custom}/__init__.py | 0 telethon/{_tl/custom => types/_custom}/adminlogevent.py | 0 telethon/{_tl/custom => types/_custom}/button.py | 0 telethon/{_tl/custom => types/_custom}/chatgetter.py | 0 telethon/{_tl/custom => types/_custom}/dialog.py | 0 telethon/{_tl/custom => types/_custom}/draft.py | 0 telethon/{_tl/custom => types/_custom}/file.py | 0 telethon/{_tl/custom => types/_custom}/forward.py | 0 telethon/{_tl/custom => types/_custom}/inlinebuilder.py | 0 telethon/{_tl/custom => types/_custom}/inlineresult.py | 0 telethon/{_tl/custom => types/_custom}/inlineresults.py | 0 telethon/{_tl/custom => types/_custom}/inputsizedfile.py | 0 telethon/{_tl/custom => types/_custom}/message.py | 0 telethon/{_tl/custom => types/_custom}/messagebutton.py | 0 telethon/{_tl/custom => types/_custom}/participantpermissions.py | 0 telethon/{_tl/custom => types/_custom}/qrlogin.py | 0 telethon/{_tl/custom => types/_custom}/sendergetter.py | 0 telethon/{_tl => types}/tlobject.py | 0 23 files changed, 0 insertions(+), 0 deletions(-) rename telethon/{_tl/core => types/_core}/__init__.py (100%) rename telethon/{_tl/core => types/_core}/gzippacked.py (100%) rename telethon/{_tl/core => types/_core}/messagecontainer.py (100%) rename telethon/{_tl/core => types/_core}/rpcresult.py (100%) rename telethon/{_tl/core => types/_core}/tlmessage.py (100%) rename telethon/{_tl/custom => types/_custom}/__init__.py (100%) rename telethon/{_tl/custom => types/_custom}/adminlogevent.py (100%) rename telethon/{_tl/custom => types/_custom}/button.py (100%) rename telethon/{_tl/custom => types/_custom}/chatgetter.py (100%) rename telethon/{_tl/custom => types/_custom}/dialog.py (100%) rename telethon/{_tl/custom => types/_custom}/draft.py (100%) rename telethon/{_tl/custom => types/_custom}/file.py (100%) rename telethon/{_tl/custom => types/_custom}/forward.py (100%) rename telethon/{_tl/custom => types/_custom}/inlinebuilder.py (100%) rename telethon/{_tl/custom => types/_custom}/inlineresult.py (100%) rename telethon/{_tl/custom => types/_custom}/inlineresults.py (100%) rename telethon/{_tl/custom => types/_custom}/inputsizedfile.py (100%) rename telethon/{_tl/custom => types/_custom}/message.py (100%) rename telethon/{_tl/custom => types/_custom}/messagebutton.py (100%) rename telethon/{_tl/custom => types/_custom}/participantpermissions.py (100%) rename telethon/{_tl/custom => types/_custom}/qrlogin.py (100%) rename telethon/{_tl/custom => types/_custom}/sendergetter.py (100%) rename telethon/{_tl => types}/tlobject.py (100%) diff --git a/telethon/_tl/core/__init__.py b/telethon/types/_core/__init__.py similarity index 100% rename from telethon/_tl/core/__init__.py rename to telethon/types/_core/__init__.py diff --git a/telethon/_tl/core/gzippacked.py b/telethon/types/_core/gzippacked.py similarity index 100% rename from telethon/_tl/core/gzippacked.py rename to telethon/types/_core/gzippacked.py diff --git a/telethon/_tl/core/messagecontainer.py b/telethon/types/_core/messagecontainer.py similarity index 100% rename from telethon/_tl/core/messagecontainer.py rename to telethon/types/_core/messagecontainer.py diff --git a/telethon/_tl/core/rpcresult.py b/telethon/types/_core/rpcresult.py similarity index 100% rename from telethon/_tl/core/rpcresult.py rename to telethon/types/_core/rpcresult.py diff --git a/telethon/_tl/core/tlmessage.py b/telethon/types/_core/tlmessage.py similarity index 100% rename from telethon/_tl/core/tlmessage.py rename to telethon/types/_core/tlmessage.py diff --git a/telethon/_tl/custom/__init__.py b/telethon/types/_custom/__init__.py similarity index 100% rename from telethon/_tl/custom/__init__.py rename to telethon/types/_custom/__init__.py diff --git a/telethon/_tl/custom/adminlogevent.py b/telethon/types/_custom/adminlogevent.py similarity index 100% rename from telethon/_tl/custom/adminlogevent.py rename to telethon/types/_custom/adminlogevent.py diff --git a/telethon/_tl/custom/button.py b/telethon/types/_custom/button.py similarity index 100% rename from telethon/_tl/custom/button.py rename to telethon/types/_custom/button.py diff --git a/telethon/_tl/custom/chatgetter.py b/telethon/types/_custom/chatgetter.py similarity index 100% rename from telethon/_tl/custom/chatgetter.py rename to telethon/types/_custom/chatgetter.py diff --git a/telethon/_tl/custom/dialog.py b/telethon/types/_custom/dialog.py similarity index 100% rename from telethon/_tl/custom/dialog.py rename to telethon/types/_custom/dialog.py diff --git a/telethon/_tl/custom/draft.py b/telethon/types/_custom/draft.py similarity index 100% rename from telethon/_tl/custom/draft.py rename to telethon/types/_custom/draft.py diff --git a/telethon/_tl/custom/file.py b/telethon/types/_custom/file.py similarity index 100% rename from telethon/_tl/custom/file.py rename to telethon/types/_custom/file.py diff --git a/telethon/_tl/custom/forward.py b/telethon/types/_custom/forward.py similarity index 100% rename from telethon/_tl/custom/forward.py rename to telethon/types/_custom/forward.py diff --git a/telethon/_tl/custom/inlinebuilder.py b/telethon/types/_custom/inlinebuilder.py similarity index 100% rename from telethon/_tl/custom/inlinebuilder.py rename to telethon/types/_custom/inlinebuilder.py diff --git a/telethon/_tl/custom/inlineresult.py b/telethon/types/_custom/inlineresult.py similarity index 100% rename from telethon/_tl/custom/inlineresult.py rename to telethon/types/_custom/inlineresult.py diff --git a/telethon/_tl/custom/inlineresults.py b/telethon/types/_custom/inlineresults.py similarity index 100% rename from telethon/_tl/custom/inlineresults.py rename to telethon/types/_custom/inlineresults.py diff --git a/telethon/_tl/custom/inputsizedfile.py b/telethon/types/_custom/inputsizedfile.py similarity index 100% rename from telethon/_tl/custom/inputsizedfile.py rename to telethon/types/_custom/inputsizedfile.py diff --git a/telethon/_tl/custom/message.py b/telethon/types/_custom/message.py similarity index 100% rename from telethon/_tl/custom/message.py rename to telethon/types/_custom/message.py diff --git a/telethon/_tl/custom/messagebutton.py b/telethon/types/_custom/messagebutton.py similarity index 100% rename from telethon/_tl/custom/messagebutton.py rename to telethon/types/_custom/messagebutton.py diff --git a/telethon/_tl/custom/participantpermissions.py b/telethon/types/_custom/participantpermissions.py similarity index 100% rename from telethon/_tl/custom/participantpermissions.py rename to telethon/types/_custom/participantpermissions.py diff --git a/telethon/_tl/custom/qrlogin.py b/telethon/types/_custom/qrlogin.py similarity index 100% rename from telethon/_tl/custom/qrlogin.py rename to telethon/types/_custom/qrlogin.py diff --git a/telethon/_tl/custom/sendergetter.py b/telethon/types/_custom/sendergetter.py similarity index 100% rename from telethon/_tl/custom/sendergetter.py rename to telethon/types/_custom/sendergetter.py diff --git a/telethon/_tl/tlobject.py b/telethon/types/tlobject.py similarity index 100% rename from telethon/_tl/tlobject.py rename to telethon/types/tlobject.py From 01061e07191558d0bdf9cd25bdaaaa05d0d0d92f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 12 Sep 2021 16:08:20 +0200 Subject: [PATCH 048/256] Sort migration guide in roughly order of importance --- readthedocs/misc/v2-migration-guide.rst | 103 +++++++++++++----------- 1 file changed, 54 insertions(+), 49 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index ccf53027..3815f2dd 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -6,8 +6,9 @@ Version 2 represents the second major version change, breaking compatibility with old code beyond the usual raw API changes in order to clean up a lot of the technical debt that has grown on the project. -This document documents all the things you should be aware of when migrating -from Telethon version 1.x to 2.0 onwards. +This document documents all the things you should be aware of when migrating from Telethon version +1.x to 2.0 onwards. It is sorted roughly from the "likely most impactful changes" to "there's a +good chance you were not relying on this to begin with". **Please read this document in full before upgrading your code to Telethon 2.0.** @@ -30,48 +31,35 @@ 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 subpackages and modules are now private --------------------------------------------- +Synchronous compatibility mode has been removed +----------------------------------------------- -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 "sync hack" (which kicked in as soon as anything from ``telethon.sync`` was imported) has been +removed. This implies: -The following subpackages are now considered private: +* The ``telethon.sync`` module is gone. +* Synchronous context-managers (``with`` as opposed to ``async with``) are no longer supported. + Most notably, you can no longer do ``with client``. It must be ``async with client`` now. +* The "smart" behaviour of the following methods has been removed and now they no longer work in + a synchronous context when the ``asyncio`` event loop was not running. This means they now need + to be used with ``await`` (or, alternatively, manually used with ``loop.run_until_complete``): + * ``start`` + * ``disconnect`` + * ``run_until_disconnected`` -* ``client`` is now ``_client``. -* ``crypto`` is now ``_crypto``. -* ``extensions`` is now ``_misc``. -* ``tl`` is now ``_tl``. - -The following modules have been moved inside ``_misc``: - -* ``entitycache.py`` -* ``helpers.py`` -* ``hints.py`` -* ``password.py`` -* ``requestiter.py` -* ``statecache.py`` -* ``utils.py`` - -// TODO review telethon/__init__.py isn't exposing more than it should +// TODO provide standalone alternative for this? -The TelegramClient is no longer made out of mixins --------------------------------------------------- - -If you were relying on any of the individual mixins that made up the client, such as -``UserMethods`` inside the ``telethon.client`` subpackage, those are now gone. -There is a single ``TelegramClient`` class now, containing everything you need. - - -Raw API methods have been renamed ---------------------------------- +Raw API methods have been renamed and are now considered private +---------------------------------------------------------------- The subpackage holding the raw API methods has been renamed from ``tl`` to ``_tl`` in order to signal that these are prone to change across minor version bumps (the ``y`` in version ``x.y.z``). +Because in Python "we're all adults", you *can* use this private module if you need to. However, +you *are* also acknowledging that this is a private module prone to change (and indeed, it will +change on layer upgrades across minor version bumps). + The ``Request`` suffix has been removed from the classes inside ``tl.functions``. The ``tl.types`` is now simply ``_tl``, and the ``tl.functions`` is now ``_tl.fn``. @@ -103,23 +91,32 @@ This serves multiple goals: // TODO this definitely generated files mapping from the original name to this new one... -Synchronous compatibility mode has been removed ------------------------------------------------ +Many subpackages and modules are now private +-------------------------------------------- -The "sync hack" (which kicked in as soon as anything from ``telethon.sync`` was imported) has been -removed. This implies: +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.sync`` module is gone. -* Synchronous context-managers (``with`` as opposed to ``async with``) are no longer supported. - Most notably, you can no longer do ``with client``. It must be ``async with client`` now. -* The "smart" behaviour of the following methods has been removed and now they no longer work in - a synchronous context when the ``asyncio`` event loop was not running. This means they now need - to be used with ``await`` (or, alternatively, manually used with ``loop.run_until_complete``): - * ``start`` - * ``disconnect`` - * ``run_until_disconnected`` +The following subpackages are now considered private: -// TODO provide standalone alternative for this? +* ``client`` is now ``_client``. +* ``crypto`` is now ``_crypto``. +* ``extensions`` is now ``_misc``. +* ``tl`` is now ``_tl``. + +The following modules have been moved inside ``_misc``: + +* ``entitycache.py`` +* ``helpers.py`` +* ``hints.py`` +* ``password.py`` +* ``requestiter.py` +* ``statecache.py`` +* ``utils.py`` + +// TODO review telethon/__init__.py isn't exposing more than it should The Conversation API has been removed @@ -134,3 +131,11 @@ just fine This approach can also be easily persisted, and you can adjust it to y your handlers much more easily. // TODO provide standalone alternative for this? + + +The TelegramClient is no longer made out of mixins +-------------------------------------------------- + +If you were relying on any of the individual mixins that made up the client, such as +``UserMethods`` inside the ``telethon.client`` subpackage, those are now gone. +There is a single ``TelegramClient`` class now, containing everything you need. From 5fd2a017b2759c598e1ea0bd5965049a003ce05c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 12 Sep 2021 16:23:55 +0200 Subject: [PATCH 049/256] Fix imports --- telethon/__init__.py | 3 +-- telethon/_crypto/authkey.py | 2 +- telethon/_misc/__init__.py | 1 - telethon/_misc/binaryreader.py | 2 +- telethon/_misc/hints.py | 4 ++-- telethon/{types => _misc}/tlobject.py | 0 telethon/types/__init__.py | 16 ++++++++++++++++ telethon_generator/generators/docs.py | 2 +- telethon_generator/generators/tlobject.py | 6 +++--- telethon_generator/parsers/errors.py | 2 +- telethon_generator/parsers/tlobject/tlobject.py | 2 +- 11 files changed, 27 insertions(+), 13 deletions(-) rename telethon/{types => _misc}/tlobject.py (100%) create mode 100644 telethon/types/__init__.py diff --git a/telethon/__init__.py b/telethon/__init__.py index fa01de5b..59ea6691 100644 --- a/telethon/__init__.py +++ b/telethon/__init__.py @@ -2,8 +2,7 @@ from ._misc import helpers # no dependencies from . import _tl # no dependencies from ._misc import utils # depends on helpers and _tl -from ._tl import custom # depends on utils -from ._misc import hints # depends on custom +from ._misc import hints # depends on types/custom from ._client.telegramclient import TelegramClient from ._network import connection diff --git a/telethon/_crypto/authkey.py b/telethon/_crypto/authkey.py index fa6fbb78..54c66c76 100644 --- a/telethon/_crypto/authkey.py +++ b/telethon/_crypto/authkey.py @@ -4,7 +4,7 @@ This module holds the AuthKey class. import struct from hashlib import sha1 -from .._misc import BinaryReader +from .._misc.binaryreader import BinaryReader class AuthKey: diff --git a/telethon/_misc/__init__.py b/telethon/_misc/__init__.py index a3c77295..71c26b8a 100644 --- a/telethon/_misc/__init__.py +++ b/telethon/_misc/__init__.py @@ -3,4 +3,3 @@ Several extensions Python is missing, such as a proper class to handle a TCP communication with support for cancelling the operation, and a utility class to read arbitrary binary data in a more comfortable way, with int/strings/etc. """ -from .binaryreader import BinaryReader diff --git a/telethon/_misc/binaryreader.py b/telethon/_misc/binaryreader.py index e5c34c7f..34648a7a 100644 --- a/telethon/_misc/binaryreader.py +++ b/telethon/_misc/binaryreader.py @@ -10,7 +10,7 @@ from struct import unpack from ..errors import TypeNotFoundError from .. import _tl from .._tl.alltlobjects import tlobjects -from .._tl import core +from ..types import core _EPOCH_NAIVE = datetime(*time.gmtime(0)[:6]) _EPOCH = _EPOCH_NAIVE.replace(tzinfo=timezone.utc) diff --git a/telethon/_misc/hints.py b/telethon/_misc/hints.py index a5299a25..47f26a8f 100644 --- a/telethon/_misc/hints.py +++ b/telethon/_misc/hints.py @@ -3,7 +3,7 @@ import typing from . import helpers from .. import _tl -from .._tl import custom +from ..types import _custom Phone = str Username = str @@ -22,7 +22,7 @@ EntityLike = typing.Union[ ] EntitiesLike = typing.Union[EntityLike, typing.Sequence[EntityLike]] -ButtonLike = typing.Union[_tl.TypeKeyboardButton, custom.Button] +ButtonLike = typing.Union[_tl.TypeKeyboardButton, _custom.Button] MarkupLike = typing.Union[ _tl.TypeReplyMarkup, ButtonLike, diff --git a/telethon/types/tlobject.py b/telethon/_misc/tlobject.py similarity index 100% rename from telethon/types/tlobject.py rename to telethon/_misc/tlobject.py diff --git a/telethon/types/__init__.py b/telethon/types/__init__.py new file mode 100644 index 00000000..ac52ff6d --- /dev/null +++ b/telethon/types/__init__.py @@ -0,0 +1,16 @@ +from .._misc.tlobject import TLObject, TLRequest +from ._custom import ( + AdminLogEvent, + Draft, + Dialog, + InputSizedFile, + MessageButton, + Forward, + Message, + Button, + InlineBuilder, + InlineResult, + InlineResults, + QRLogin, + ParticipantPermissions, +) diff --git a/telethon_generator/generators/docs.py b/telethon_generator/generators/docs.py index d2da55d1..8b46e4d1 100755 --- a/telethon_generator/generators/docs.py +++ b/telethon_generator/generators/docs.py @@ -9,7 +9,7 @@ from pathlib import Path from ..docswriter import DocsWriter from ..parsers import TLObject, Usability -from .._misc.utils import snake_to_camel_case +from ..utils import snake_to_camel_case CORE_TYPES = { 'int', 'long', 'int128', 'int256', 'double', diff --git a/telethon_generator/generators/tlobject.py b/telethon_generator/generators/tlobject.py index 04003a6b..25c42e1e 100644 --- a/telethon_generator/generators/tlobject.py +++ b/telethon_generator/generators/tlobject.py @@ -7,7 +7,7 @@ from collections import defaultdict from zlib import crc32 from ..sourcebuilder import SourceBuilder -from .._misc.utils import snake_to_camel_case +from ..utils import snake_to_camel_case AUTO_GEN_NOTICE = \ '"""File generated by TLObjects\' generator. All changes will be ERASED"""' @@ -61,10 +61,10 @@ def _write_modules( builder.writeln(AUTO_GEN_NOTICE) if kind == 'TLObject': - builder.writeln('from .tlobject import TLObject, TLRequest') + builder.writeln('from .._misc.tlobject import TLObject, TLRequest') builder.writeln('from . import fn') else: - builder.writeln('from .. import TLObject, TLRequest') + builder.writeln('from ..._misc.tlobject import TLObject, TLRequest') builder.writeln('from typing import Optional, List, ' 'Union, TYPE_CHECKING') diff --git a/telethon_generator/parsers/errors.py b/telethon_generator/parsers/errors.py index 9bac2142..04cd3412 100644 --- a/telethon_generator/parsers/errors.py +++ b/telethon_generator/parsers/errors.py @@ -1,7 +1,7 @@ import csv import re -from .._misc.utils import snake_to_camel_case +from ..utils import snake_to_camel_case # Core base classes depending on the integer error code KNOWN_BASE_CLASSES = { diff --git a/telethon_generator/parsers/tlobject/tlobject.py b/telethon_generator/parsers/tlobject/tlobject.py index 0f753fa2..60b9e996 100644 --- a/telethon_generator/parsers/tlobject/tlobject.py +++ b/telethon_generator/parsers/tlobject/tlobject.py @@ -2,7 +2,7 @@ import re import struct import zlib -from ..._misc.utils import snake_to_camel_case +from ...utils import snake_to_camel_case # https://github.com/telegramdesktop/tdesktop/blob/4bf66cb6e93f3965b40084771b595e93d0b11bcd/Telegram/SourceFiles/codegen/scheme/codegen_scheme.py#L57-L62 WHITELISTED_MISMATCHING_IDS = { From 499fc9f603c62f63c53120e5ecdbd43b575ebcb5 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 12 Sep 2021 16:58:06 +0200 Subject: [PATCH 050/256] Move alltlobjects.py and fix imports --- telethon/__init__.py | 1 - telethon/_client/auth.py | 6 +- telethon/_client/bots.py | 6 +- telethon/_client/buttons.py | 8 +- telethon/_client/chats.py | 12 +-- telethon/_client/dialogs.py | 6 +- telethon/_client/downloads.py | 4 +- telethon/_client/telegrambaseclient.py | 3 +- telethon/_client/telegramclient.py | 104 +++++++++++----------- telethon/_client/uploads.py | 4 +- telethon/_crypto/rsa.py | 6 +- telethon/_misc/binaryreader.py | 7 +- telethon/_misc/markdown.py | 3 +- telethon/_misc/messagepacker.py | 3 +- telethon/_network/authenticator.py | 2 +- telethon/_network/mtprotoplainsender.py | 2 +- telethon/_network/mtprotosender.py | 6 +- telethon/_network/mtprotostate.py | 7 +- telethon/events/album.py | 12 +-- telethon/events/callbackquery.py | 8 +- telethon/events/common.py | 7 +- telethon/events/inlinequery.py | 6 +- telethon/events/userupdate.py | 6 +- telethon/sessions/memory.py | 8 +- telethon/types/_core/gzippacked.py | 6 +- telethon/types/_core/messagecontainer.py | 2 +- telethon/types/_core/rpcresult.py | 7 +- telethon/types/_core/tlmessage.py | 4 +- telethon/types/_custom/dialog.py | 6 +- telethon/types/_custom/draft.py | 6 +- telethon/types/_custom/message.py | 4 +- telethon_generator/generators/tlobject.py | 31 +++---- 32 files changed, 145 insertions(+), 158 deletions(-) diff --git a/telethon/__init__.py b/telethon/__init__.py index 59ea6691..edbd4126 100644 --- a/telethon/__init__.py +++ b/telethon/__init__.py @@ -6,7 +6,6 @@ from ._misc import hints # depends on types/custom from ._client.telegramclient import TelegramClient from ._network import connection -from ._tl.custom import Button from . import version, events, utils, errors __version__ = version.__version__ diff --git a/telethon/_client/auth.py b/telethon/_client/auth.py index 4ae32c43..992d44e1 100644 --- a/telethon/_client/auth.py +++ b/telethon/_client/auth.py @@ -7,7 +7,7 @@ import warnings from .._misc import utils, helpers, password as pwd_mod from .. import errors, _tl -from .._tl import custom +from ..types import _custom if typing.TYPE_CHECKING: from .telegramclient import TelegramClient @@ -343,8 +343,8 @@ async def send_code_request( return result -async def qr_login(self: 'TelegramClient', ignored_ids: typing.List[int] = None) -> custom.QRLogin: - qr_login = custom.QRLogin(self, ignored_ids or []) +async def qr_login(self: 'TelegramClient', ignored_ids: typing.List[int] = None) -> _custom.QRLogin: + qr_login = _custom.QRLogin(self, ignored_ids or []) await qr_login.recreate() return qr_login diff --git a/telethon/_client/bots.py b/telethon/_client/bots.py index 8c2d50fe..9360d7f3 100644 --- a/telethon/_client/bots.py +++ b/telethon/_client/bots.py @@ -1,7 +1,7 @@ import typing from .. import hints, _tl -from .._tl import custom +from ..types import _custom if typing.TYPE_CHECKING: from .telegramclient import TelegramClient @@ -14,7 +14,7 @@ async def inline_query( *, entity: 'hints.EntityLike' = None, offset: str = None, - geo_point: '_tl.GeoPoint' = None) -> custom.InlineResults: + geo_point: '_tl.GeoPoint' = None) -> _custom.InlineResults: bot = await self.get_input_entity(bot) if entity: peer = await self.get_input_entity(entity) @@ -29,4 +29,4 @@ async def inline_query( 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) diff --git a/telethon/_client/buttons.py b/telethon/_client/buttons.py index 897b4703..fbd0f51c 100644 --- a/telethon/_client/buttons.py +++ b/telethon/_client/buttons.py @@ -2,7 +2,7 @@ import typing from .._misc import utils, hints from .. import _tl -from .._tl import custom +from ..types import _custom def build_reply_markup( @@ -32,7 +32,7 @@ def build_reply_markup( for row in buttons: current = [] for button in row: - if isinstance(button, custom.Button): + if isinstance(button, _custom.Button): if button.resize is not None: resize = button.resize if button.single_use is not None: @@ -41,10 +41,10 @@ def build_reply_markup( selective = button.selective button = button.button - elif isinstance(button, custom.MessageButton): + elif isinstance(button, _custom.MessageButton): button = button.button - inline = custom.Button._is_inline(button) + inline = _custom.Button._is_inline(button) is_inline |= inline is_normal |= not inline diff --git a/telethon/_client/chats.py b/telethon/_client/chats.py index 0fb88ed8..0ff93c02 100644 --- a/telethon/_client/chats.py +++ b/telethon/_client/chats.py @@ -5,8 +5,8 @@ import string import typing from .. import hints, errors, _tl -from .._misc import helpers, utils, requestiter -from .._tl import custom +from .._misc import helpers, utils, requestiter, tlobject +from ..types import _custom if typing.TYPE_CHECKING: from .telegramclient import TelegramClient @@ -302,7 +302,7 @@ class _AdminLogIter(requestiter.RequestIter): ev.action.message._finish_init( self.client, entities, self.entity) - self.buffer.append(custom.AdminLogEvent(ev, entities)) + self.buffer.append(_custom.AdminLogEvent(ev, entities)) if len(r.events) < self.request.limit: return True @@ -516,7 +516,7 @@ def action( except KeyError: raise ValueError( 'No such action "{}"'.format(action)) from None - elif not isinstance(action, _tl.TLObject) or action.SUBCLASS_OF_ID != 0x20b2cc21: + elif not isinstance(action, tlobject.TLObject) or action.SUBCLASS_OF_ID != 0x20b2cc21: # 0x20b2cc21 = crc32(b'SendMessageAction') if isinstance(action, type): raise ValueError('You must pass an instance, not the class') @@ -716,7 +716,7 @@ async def get_permissions( entity, user )) - return custom.ParticipantPermissions(participant.participant, False) + return _custom.ParticipantPermissions(participant.participant, False) elif helpers._entity_type(entity) == helpers._EntityType.CHAT: chat = await self(_tl.fn.messages.GetFullChat( entity @@ -725,7 +725,7 @@ async def get_permissions( user = await self.get_me(input_peer=True) for participant in chat.full_chat.participants.participants: if participant.user_id == user.user_id: - return custom.ParticipantPermissions(participant, True) + return _custom.ParticipantPermissions(participant, True) raise errors.UserNotParticipantError(None) raise ValueError('You must pass either a channel or a chat') diff --git a/telethon/_client/dialogs.py b/telethon/_client/dialogs.py index bfd76f61..850e3ac9 100644 --- a/telethon/_client/dialogs.py +++ b/telethon/_client/dialogs.py @@ -5,7 +5,7 @@ import typing from .. import hints, errors, _tl from .._misc import helpers, utils, requestiter -from .._tl import custom +from ..types import _custom _MAX_CHUNK_SIZE = 100 @@ -80,7 +80,7 @@ class _DialogsIter(requestiter.RequestIter): # Real world example: https://t.me/TelethonChat/271471 continue - cd = custom.Dialog(self.client, d, entities, message) + cd = _custom.Dialog(self.client, d, entities, message) if cd.dialog.pts: self.client._channel_pts[cd.id] = cd.dialog.pts @@ -128,7 +128,7 @@ class _DraftsIter(requestiter.RequestIter): for x in itertools.chain(r.users, r.chats)} self.buffer.extend( - custom.Draft(self.client, entities[utils.get_peer_id(d.peer)], d.draft) + _custom.Draft(self.client, entities[utils.get_peer_id(d.peer)], d.draft) for d in items ) diff --git a/telethon/_client/downloads.py b/telethon/_client/downloads.py index 25a461cb..6cd34b10 100644 --- a/telethon/_client/downloads.py +++ b/telethon/_client/downloads.py @@ -7,7 +7,7 @@ import inspect import asyncio from .._crypto import AES -from .._misc import utils, helpers, requestiter +from .._misc import utils, helpers, requestiter, tlobject from .. import errors, hints, _tl try: @@ -198,7 +198,7 @@ async def download_profile_photo( ENTITIES = (0x2da17977, 0xc5af5d94, 0x1f4661b9, 0xd49a2697) # ('InputPeer', 'InputUser', 'InputChannel') INPUTS = (0xc91c90b6, 0xe669bf46, 0x40f202fd) - if not isinstance(entity, _tl.TLObject) or entity.SUBCLASS_OF_ID in INPUTS: + if not isinstance(entity, tlobject.TLObject) or entity.SUBCLASS_OF_ID in INPUTS: entity = await self.get_entity(entity) thumb = -1 if download_big else 0 diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index b45ac6d3..0ac127f0 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -11,7 +11,6 @@ from .. import version, helpers, __name__ as __base_name__, _tl from .._crypto import rsa from .._misc import markdown, entitycache, statecache from .._network import MTProtoSender, Connection, ConnectionTcpFull, TcpMTProxy -from .._tl.alltlobjects import LAYER from ..sessions import Session, SQLiteSession, MemorySession DEFAULT_DC_ID = 2 @@ -322,7 +321,7 @@ async def connect(self: 'TelegramClient') -> None: self._init_request.query = _tl.fn.help.GetConfig() await self._sender.send(_tl.fn.InvokeWithLayer( - LAYER, self._init_request + _tl.LAYER, self._init_request )) self._updates_handle = self.loop.create_task(self._update_loop()) diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 3596a506..85b28cd3 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -9,7 +9,7 @@ from . import ( telegrambaseclient, updates, uploads, users ) from .. import helpers, version, _tl -from .._tl import custom +from ..types import _custom from .._network import ConnectionTcpFull from ..events.common import EventBuilder, EventCommon @@ -500,7 +500,7 @@ class TelegramClient: """ return await auth.send_code_request(**locals()) - async def qr_login(self: 'TelegramClient', ignored_ids: typing.List[int] = None) -> custom.QRLogin: + async def qr_login(self: 'TelegramClient', ignored_ids: typing.List[int] = None) -> _custom.QRLogin: """ Initiates the QR login procedure. @@ -630,7 +630,7 @@ class TelegramClient: *, entity: 'hints.EntityLike' = None, offset: str = None, - geo_point: '_tl.GeoPoint' = None) -> custom.InlineResults: + geo_point: '_tl.GeoPoint' = None) -> _custom.InlineResults: """ Makes an inline query to the specified bot (``@vote New Poll``). @@ -658,8 +658,8 @@ class TelegramClient: for localised results. Available under some bots. Returns - A list of `custom.InlineResult - `. + A list of `_custom.InlineResult + `. Example .. code-block:: python @@ -923,7 +923,7 @@ class TelegramClient: If `True`, events related to group calls will be returned. Yields - Instances of `AdminLogEvent `. + Instances of `AdminLogEvent `. Example .. code-block:: python @@ -1161,8 +1161,8 @@ class TelegramClient: .. note:: Users may be able to identify the anonymous admin by its - custom title, so additional care is needed when using both - ``anonymous`` and custom titles. For example, if multiple + _custom title, so additional care is needed when using both + ``anonymous`` and _custom titles. For example, if multiple anonymous admins share the same title, users won't be able to distinguish them. @@ -1178,7 +1178,7 @@ class TelegramClient: permissions, but you can still disable those you need. title (`str`, optional): - The custom title (also known as "rank") to show for this admin. + The _custom title (also known as "rank") to show for this admin. This text will be shown instead of the "admin" badge. This will only work in channels and megagroups. @@ -1340,7 +1340,7 @@ class TelegramClient: The user to kick. Returns - Returns the service `Message ` + Returns the service `Message ` produced about a user being kicked, if any. Example @@ -1359,7 +1359,7 @@ class TelegramClient: self: 'TelegramClient', entity: 'hints.EntityLike', user: 'hints.EntityLike' = None - ) -> 'typing.Optional[custom.ParticipantPermissions]': + ) -> 'typing.Optional[_custom.ParticipantPermissions]': """ Fetches the permissions of a user in a specific chat or channel or get Default Restricted Rights of Chat or Channel. @@ -1377,7 +1377,7 @@ class TelegramClient: Target user. Returns - A `ParticipantPermissions ` + A `ParticipantPermissions ` instance. Refer to its documentation to see what properties are available. @@ -1509,7 +1509,7 @@ class TelegramClient: Alias for `folder`. If unspecified, all will be returned, `False` implies ``folder=0`` and `True` implies ``folder=1``. Yields - Instances of `Dialog `. + Instances of `Dialog `. Example .. code-block:: python @@ -1563,7 +1563,7 @@ class TelegramClient: If left unspecified, all draft messages will be returned. Yields - Instances of `Draft `. + Instances of `Draft `. Example .. code-block:: python @@ -1670,7 +1670,7 @@ class TelegramClient: 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() `. + See also `Dialog.delete() `. Arguments entity (entities): @@ -1765,10 +1765,10 @@ class TelegramClient: ``cryptg`` (through ``pip install cryptg``) so that decrypting the received data is done in C instead of Python (much faster). - See also `Message.download_media() `. + See also `Message.download_media() `. Arguments - message (`Message ` | :tl:`Media`): + message (`Message ` | :tl:`Media`): The media or message containing the media that will be downloaded. file (`str` | `file`, optional): @@ -2186,7 +2186,7 @@ class TelegramClient: All other parameter will be ignored for this, except `entity`. Yields - Instances of `Message `. + Instances of `Message `. Example .. code-block:: python @@ -2232,7 +2232,7 @@ class TelegramClient: specified it makes sense that it should return the entirety of it. If `ids` is present in the *named* arguments and is not a list, - a single `Message ` will be + a single `Message ` will be returned for convenience instead of a list. Example @@ -2278,7 +2278,7 @@ class TelegramClient: Sends a message to the specified user, chat or channel. The default parse mode is the same as the official applications - (a custom flavour of markdown). ``**bold**, `code` or __italic__`` + (a _custom flavour of markdown). ``**bold**, `code` or __italic__`` are available. In addition you can send ``[links](https://example.com)`` and ``[mentions](@username)`` (or using IDs like in the Bot API: ``[mention](tg://user?id=123456789)``) and ``pre`` blocks with three @@ -2288,14 +2288,14 @@ class TelegramClient: is also done through this method. Simply send ``'/start data'`` to the bot. - See also `Message.respond() ` - and `Message.reply() `. + See also `Message.respond() ` + and `Message.reply() `. Arguments entity (`entity`): To who will it be sent. - message (`str` | `Message `): + message (`str` | `Message `): The message to be sent, or another message object to resend. The maximum length for a message is 35,000 bytes or 4,096 @@ -2303,7 +2303,7 @@ class TelegramClient: and you should slice them manually if the text to send is longer than said length. - reply_to (`int` | `Message `, optional): + reply_to (`int` | `Message `, optional): Whether to reply to a message or not. If an integer is provided, it should be the ID of the message that it should reply to. @@ -2344,7 +2344,7 @@ class TelegramClient: clear_draft (`bool`, optional): Whether the existing draft should be cleared or not. - buttons (`list`, `custom.Button `, :tl:`KeyboardButton`): + buttons (`list`, `_custom.Button `, :tl:`KeyboardButton`): The matrix (list of lists), row list or button to be shown after sending the message. This parameter will only work if you have signed in as a bot. You can also pass your own @@ -2378,7 +2378,7 @@ class TelegramClient: it will be scheduled to be automatically sent at a later time. - comment_to (`int` | `Message `, optional): + comment_to (`int` | `Message `, optional): Similar to ``reply_to``, but replies in the linked group of a broadcast channel instead (effectively leaving a "comment to" the specified message). @@ -2387,7 +2387,7 @@ class TelegramClient: no linked chat, `telethon.errors.sgIdInvalidError` is raised. Returns - The sent `custom.Message `. + The sent `_custom.Message `. Example .. code-block:: python @@ -2466,13 +2466,13 @@ class TelegramClient: (the "forwarded from" text), you should use `send_message` with the original message instead. This will send a copy of it. - See also `Message.forward_to() `. + See also `Message.forward_to() `. Arguments entity (`entity`): To which entity the message(s) will be forwarded. - messages (`list` | `int` | `Message `): + messages (`list` | `int` | `Message `): The message(s) to forward, or their integer IDs. from_peer (`entity`): @@ -2502,7 +2502,7 @@ class TelegramClient: at a later time. Returns - The list of forwarded `Message `, + The list of forwarded `Message `, or a single one if a list wasn't provided as input. Note that if all messages are invalid (i.e. deleted) the call @@ -2560,10 +2560,10 @@ class TelegramClient: """ Edits the given message to change its text or media. - See also `Message.edit() `. + See also `Message.edit() `. Arguments - entity (`entity` | `Message `): + entity (`entity` | `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 @@ -2573,16 +2573,16 @@ class TelegramClient: which is the only way to edit messages that were sent after the user selects an inline query result. - message (`int` | `Message ` | `str`): + message (`int` | `Message ` | `str`): The ID of the message (or `Message - ` itself) to be edited. + ` itself) to be edited. If the `entity` was a `Message - `, then this message + `, then this message will be treated as the new text. text (`str`, optional): The new text of the message. Does nothing if the `entity` - was a `Message `. + was a `Message `. parse_mode (`object`, optional): See the `TelegramClient.parse_mode @@ -2618,7 +2618,7 @@ class TelegramClient: force_document (`bool`, optional): Whether to send the given file as a document or not. - buttons (`list`, `custom.Button `, :tl:`KeyboardButton`): + buttons (`list`, `_custom.Button `, :tl:`KeyboardButton`): The matrix (list of lists), row list or button to be shown after sending the message. This parameter will only work if you have signed in as a bot. You can also pass your own @@ -2640,7 +2640,7 @@ class TelegramClient: trying to edit a message that was sent via inline bots. Returns - The edited `Message `, + The edited `Message `, unless `entity` was a :tl:`InputBotInlineMessageID` in which case this method returns a boolean. @@ -2678,7 +2678,7 @@ class TelegramClient: """ Deletes the given messages, optionally "for everyone". - See also `Message.delete() `. + See also `Message.delete() `. .. warning:: @@ -2693,7 +2693,7 @@ class TelegramClient: be `None` for normal chats, but **must** be present for channels and megagroups. - message_ids (`list` | `int` | `Message `): + message_ids (`list` | `int` | `Message `): The IDs (or ID) or messages to be deleted. revoke (`bool`, optional): @@ -2741,13 +2741,13 @@ class TelegramClient: including such ID will be marked as read (for all messages whose ID ≤ max_id). - See also `Message.mark_read() `. + See also `Message.mark_read() `. Arguments entity (`entity`): The chat where these messages are located. - message (`list` | `Message `): + message (`list` | `Message `): Either a list of messages or a single message. max_id (`int`): @@ -2787,13 +2787,13 @@ class TelegramClient: The default behaviour is to *not* notify members, unlike the official applications. - See also `Message.pin() `. + See also `Message.pin() `. Arguments entity (`entity`): The chat where the message should be pinned. - message (`int` | `Message `): + message (`int` | `Message `): The message or the message ID to pin. If it's `None`, all messages will be unpinned instead. @@ -2826,13 +2826,13 @@ class TelegramClient: If no message ID is specified, all pinned messages will be unpinned. - See also `Message.unpin() `. + See also `Message.unpin() `. Arguments entity (`entity`): The chat where the message should be pinned. - message (`int` | `Message `): + message (`int` | `Message `): The message or the message ID to unpin. If it's `None`, all messages will be unpinned instead. @@ -3277,7 +3277,7 @@ class TelegramClient: A callback function accepting two parameters: ``(sent bytes, total)``. - reply_to (`int` | `Message `): + reply_to (`int` | `Message `): Same as `reply_to` from `send_message`. attributes (`list`, optional): @@ -3318,7 +3318,7 @@ class TelegramClient: If `True` the video will be sent as a video note, also known as a round video message. - buttons (`list`, `custom.Button `, :tl:`KeyboardButton`): + buttons (`list`, `_custom.Button `, :tl:`KeyboardButton`): The matrix (list of lists), row list or button to be shown after sending the message. This parameter will only work if you have signed in as a bot. You can also pass your own @@ -3345,7 +3345,7 @@ class TelegramClient: it will be scheduled to be automatically sent at a later time. - comment_to (`int` | `Message `, optional): + comment_to (`int` | `Message `, optional): Similar to ``reply_to``, but replies in the linked group of a broadcast channel instead (effectively leaving a "comment to" the specified message). @@ -3366,7 +3366,7 @@ class TelegramClient: as text documents, which will fail with ``TtlMediaInvalidError``. Returns - The `Message ` (or messages) + The `Message ` (or messages) containing the sent file, or messages if a list of them was passed. Example @@ -3381,7 +3381,7 @@ class TelegramClient: await client.send_file(chat, '/my/songs/song.mp3', voice_note=True) await client.send_file(chat, '/my/videos/video.mp4', video_note=True) - # Custom thumbnails + # _custom thumbnails await client.send_file(chat, '/my/documents/doc.txt', thumb='photo.jpg') # Only documents @@ -3482,7 +3482,7 @@ class TelegramClient: Returns :tl:`InputFileBig` if the file size is larger than 10MB, - `InputSizedFile ` + `InputSizedFile ` (subclass of :tl:`InputFile`) otherwise. Example diff --git a/telethon/_client/uploads.py b/telethon/_client/uploads.py index c8fbcea7..fb5bfc0c 100644 --- a/telethon/_client/uploads.py +++ b/telethon/_client/uploads.py @@ -11,7 +11,7 @@ from .._crypto import AES from .._misc import utils, helpers from .. import hints, _tl -from .._tl import custom +from ..types import _custom try: import PIL @@ -363,7 +363,7 @@ async def upload_file( if is_big: return _tl.InputFileBig(file_id, part_count, file_name) else: - return custom.InputSizedFile( + return _custom.InputSizedFile( file_id, part_count, file_name, md5=hash_md5, size=file_size ) diff --git a/telethon/_crypto/rsa.py b/telethon/_crypto/rsa.py index d1f1b588..eca09743 100644 --- a/telethon/_crypto/rsa.py +++ b/telethon/_crypto/rsa.py @@ -11,7 +11,7 @@ except ImportError: rsa = None raise ImportError('Missing module "rsa", please install via pip.') -from .. import _tl +from .._misc import tlobject # {fingerprint: (Crypto.PublicKey.RSA._RSAobj, old)} dictionary @@ -41,8 +41,8 @@ def _compute_fingerprint(key): :param key: the Crypto.RSA key. :return: its 8-bytes-long fingerprint. """ - n = _tl.TLObject.serialize_bytes(get_byte_array(key.n)) - e = _tl.TLObject.serialize_bytes(get_byte_array(key.e)) + n = tlobject.TLObject.serialize_bytes(get_byte_array(key.n)) + e = tlobject.TLObject.serialize_bytes(get_byte_array(key.e)) # Telegram uses the last 8 bytes as the fingerprint return struct.unpack(' 1: return super().filter(event) - class Event(EventCommon, custom.sendergetter.SenderGetter): + class Event(EventCommon, _custom.sendergetter.SenderGetter): """ Represents the event of a new album. Members: - messages (Sequence[`Message `]): + messages (Sequence[`Message `]): The list of messages belonging to the same album. """ def __init__(self, messages): @@ -160,7 +160,7 @@ class Album(EventBuilder): super().__init__(chat_peer=chat_peer, msg_id=message.id, broadcast=bool(message.post)) - custom.sendergetter.SenderGetter.__init__(self, message.sender_id) + _custom.sendergetter.SenderGetter.__init__(self, message.sender_id) self.messages = messages def _set_client(self, client): @@ -217,7 +217,7 @@ class Album(EventBuilder): @property def forward(self): """ - The `Forward ` + The `Forward ` information for the first message in the album if it was forwarded. """ # Each individual message in an album all reply to the same message @@ -229,7 +229,7 @@ class Album(EventBuilder): async def get_reply_message(self): """ - The `Message ` + The `Message ` that this album is replying to, or `None`. The result will be cached after its first use. diff --git a/telethon/events/callbackquery.py b/telethon/events/callbackquery.py index f850ecd5..6d8ee00d 100644 --- a/telethon/events/callbackquery.py +++ b/telethon/events/callbackquery.py @@ -4,7 +4,7 @@ import struct from .common import EventBuilder, EventCommon, name_inner_event from .._misc import utils from .. import _tl -from .._tl import custom +from ..types import _custom @name_inner_event @@ -123,7 +123,7 @@ class CallbackQuery(EventBuilder): return self.func(event) return True - class Event(EventCommon, custom.sendergetter.SenderGetter): + class Event(EventCommon, _custom.sendergetter.SenderGetter): """ Represents the event of a new callback query. @@ -141,7 +141,7 @@ class CallbackQuery(EventBuilder): """ def __init__(self, query, peer, msg_id): super().__init__(peer, msg_id=msg_id) - custom.sendergetter.SenderGetter.__init__(self, query.user_id) + _custom.sendergetter.SenderGetter.__init__(self, query.user_id) self.query = query self.data_match = None self.pattern_match = None @@ -308,7 +308,7 @@ class CallbackQuery(EventBuilder): .. note:: This method won't respect the previous message unlike - `Message.edit `, + `Message.edit `, since the message object is normally not present. """ self._client.loop.create_task(self.answer()) diff --git a/telethon/events/common.py b/telethon/events/common.py index cce243e6..405537d2 100644 --- a/telethon/events/common.py +++ b/telethon/events/common.py @@ -2,8 +2,9 @@ import abc import asyncio import warnings -from .._misc import utils -from .._tl.custom.chatgetter import ChatGetter +from .. import _tl +from .._misc import utils, tlobject +from ..types._custom.chatgetter import ChatGetter async def _into_id_set(client, chats): @@ -25,7 +26,7 @@ async def _into_id_set(client, chats): utils.get_peer_id(_tl.PeerChat(chat)), utils.get_peer_id(_tl.PeerChannel(chat)), }) - elif isinstance(chat, _tl.TLObject) and chat.SUBCLASS_OF_ID == 0x2d45687: + elif isinstance(chat, tlobject.TLObject) and chat.SUBCLASS_OF_ID == 0x2d45687: # 0x2d45687 == crc32(b'Peer') result.add(utils.get_peer_id(chat)) else: diff --git a/telethon/events/inlinequery.py b/telethon/events/inlinequery.py index f2c13d3d..0af44dd0 100644 --- a/telethon/events/inlinequery.py +++ b/telethon/events/inlinequery.py @@ -6,7 +6,7 @@ import asyncio from .common import EventBuilder, EventCommon, name_inner_event from .._misc import utils from .. import _tl -from .._tl import custom +from ..types import _custom @name_inner_event @@ -74,7 +74,7 @@ class InlineQuery(EventBuilder): return super().filter(event) - class Event(EventCommon, custom.sendergetter.SenderGetter): + class Event(EventCommon, _custom.sendergetter.SenderGetter): """ Represents the event of a new callback query. @@ -91,7 +91,7 @@ class InlineQuery(EventBuilder): """ def __init__(self, query): super().__init__(chat_peer=_tl.PeerUser(query.user_id)) - custom.sendergetter.SenderGetter.__init__(self, query.user_id) + _custom.sendergetter.SenderGetter.__init__(self, query.user_id) self.query = query self.pattern_match = None self._answered = False diff --git a/telethon/events/userupdate.py b/telethon/events/userupdate.py index 8144cadb..61db39ea 100644 --- a/telethon/events/userupdate.py +++ b/telethon/events/userupdate.py @@ -4,7 +4,7 @@ import functools from .common import EventBuilder, EventCommon, name_inner_event from .._misc import utils from .. import _tl -from .._tl import custom +from ..types import _custom # TODO Either the properties are poorly named or they should be @@ -65,7 +65,7 @@ class UserUpdate(EventBuilder): return cls.Event(update.user_id, typing=update.action) - class Event(EventCommon, custom.sendergetter.SenderGetter): + class Event(EventCommon, _custom.sendergetter.SenderGetter): """ Represents the event of a user update such as gone online, started typing, etc. @@ -87,7 +87,7 @@ class UserUpdate(EventBuilder): """ def __init__(self, peer, *, status=None, chat_peer=None, typing=None): super().__init__(chat_peer or peer) - custom.sendergetter.SenderGetter.__init__(self, utils.get_peer_id(peer)) + _custom.sendergetter.SenderGetter.__init__(self, utils.get_peer_id(peer)) self.status = status self.action = typing diff --git a/telethon/sessions/memory.py b/telethon/sessions/memory.py index 9f5314a3..82067101 100644 --- a/telethon/sessions/memory.py +++ b/telethon/sessions/memory.py @@ -1,7 +1,7 @@ from enum import Enum from .abstract import Session -from .._misc import utils +from .._misc import utils, tlobject from .. import _tl @@ -89,7 +89,7 @@ class MemorySession(Session): return id, hash, username, phone, name def _entity_to_row(self, e): - if not isinstance(e, _tl.TLObject): + if not isinstance(e, tlobject.TLObject): return try: p = utils.get_input_peer(e, allow_self=False) @@ -118,7 +118,7 @@ class MemorySession(Session): ) def _entities_to_rows(self, tlo): - if not isinstance(tlo, _tl.TLObject) and utils.is_list_like(tlo): + if not isinstance(tlo, tlobject.TLObject) and utils.is_list_like(tlo): # This may be a list of users already for instance entities = tlo else: @@ -189,7 +189,7 @@ class MemorySession(Session): return utils.get_input_peer(key) except (AttributeError, TypeError): # Not a TLObject or can't be cast into InputPeer - if isinstance(key, _tl.TLObject): + if isinstance(key, tlobject.TLObject): key = utils.get_peer_id(key) exact = True else: diff --git a/telethon/types/_core/gzippacked.py b/telethon/types/_core/gzippacked.py index fb4094e4..fd153196 100644 --- a/telethon/types/_core/gzippacked.py +++ b/telethon/types/_core/gzippacked.py @@ -1,10 +1,10 @@ import gzip import struct -from .. import TLObject +from ..._misc import tlobject -class GzipPacked(TLObject): +class GzipPacked(tlobject.TLObject): CONSTRUCTOR_ID = 0x3072cfa1 def __init__(self, data): @@ -26,7 +26,7 @@ class GzipPacked(TLObject): def __bytes__(self): return struct.pack(' Date: Mon, 13 Sep 2021 20:37:29 +0200 Subject: [PATCH 051/256] Make custom.Message functional --- readthedocs/misc/v2-migration-guide.rst | 26 ++ telethon/_client/chats.py | 12 +- telethon/_client/dialogs.py | 8 +- telethon/_client/messageparse.py | 17 +- telethon/_client/messages.py | 13 +- telethon/_client/telegramclient.py | 3 + telethon/events/album.py | 6 +- telethon/events/chataction.py | 1 + telethon/events/newmessage.py | 1 + telethon/types/_custom/message.py | 424 ++++++++++++------------ 10 files changed, 261 insertions(+), 250 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 3815f2dd..483c5287 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -119,6 +119,32 @@ The following modules have been moved inside ``_misc``: // TODO review telethon/__init__.py isn't exposing more than it should +The custom.Message class and the way it is used has changed +----------------------------------------------------------- + +It no longer inherits ``TLObject``, and rather than trying to mimick Telegram's ``Message`` +constructor, it now takes two parameters: a ``TelegramClient`` instance and a ``_tl.Message``. +As a benefit, you can now more easily reconstruct instances of this type from a previously-stored +``_tl.Message`` instance. + +There are no public attributes. Instead, they are now properties which forward the values into and +from the private ``_message`` field. As a benefit, the documentation will now be easier to follow. +However, you can no longer use ``del`` on these. + +The ``_tl.Message.media`` attribute will no longer be ``None`` when using raw API if the media was +``messageMediaEmpty``. As a benefit, you can now actually distinguish between no media and empty +media. The ``Message.media`` property as returned by friendly methods will still be ``None`` on +empty media. + +The ``telethon.tl.patched`` hack has been removed. + +In order to avoid breaking more code than strictly necessary, ``.raw_text`` will remain a synonym +of ``.message``, and ``.text`` will still be the text formatted through the ``client.parse_mode``. +However, you're encouraged to change uses of ``.raw_text`` with ``.message``, and ``.text`` with +either ``.md_text`` or ``.html_text`` as needed. This is because both ``.text`` and ``.raw_text`` +may disappear in future versions, and their behaviour is not immediately obvious. + + The Conversation API has been removed ------------------------------------- diff --git a/telethon/_client/chats.py b/telethon/_client/chats.py index 0ff93c02..7eb6a2a1 100644 --- a/telethon/_client/chats.py +++ b/telethon/_client/chats.py @@ -291,16 +291,16 @@ class _AdminLogIter(requestiter.RequestIter): for ev in r.events: if isinstance(ev.action, _tl.ChannelAdminLogEventActionEditMessage): - ev.action.prev_message._finish_init( - self.client, entities, self.entity) + ev.action.prev_message = _custom.Message._new( + self.client, ev.action.prev_message, entities, self.entity) - ev.action.new_message._finish_init( - self.client, entities, self.entity) + ev.action.new_message = _custom.Message._new( + self.client, ev.action.new_message, entities, self.entity) elif isinstance(ev.action, _tl.ChannelAdminLogEventActionDeleteMessage): - ev.action.message._finish_init( - self.client, entities, self.entity) + ev.action.message = _custom.Message._new( + self.client, ev.action.message, entities, self.entity) self.buffer.append(_custom.AdminLogEvent(ev, entities)) diff --git a/telethon/_client/dialogs.py b/telethon/_client/dialogs.py index 850e3ac9..b293edca 100644 --- a/telethon/_client/dialogs.py +++ b/telethon/_client/dialogs.py @@ -58,10 +58,10 @@ class _DialogsIter(requestiter.RequestIter): for x in itertools.chain(r.users, r.chats) if not isinstance(x, (_tl.UserEmpty, _tl.ChatEmpty))} - messages = {} - for m in r.messages: - m._finish_init(self.client, entities, None) - messages[_dialog_message_key(m.peer_id, m.id)] = m + messages = { + _dialog_message_key(m.peer_id, m.id): _custom.Message._new(self.client, m, entities, None) + for m in r.messages + } for d in r.dialogs: # We check the offset date here because Telegram may ignore it diff --git a/telethon/_client/messageparse.py b/telethon/_client/messageparse.py index 9f1f3b70..69d438fd 100644 --- a/telethon/_client/messageparse.py +++ b/telethon/_client/messageparse.py @@ -3,6 +3,7 @@ import re import typing from .. import helpers, utils, _tl +from ..types import _custom if typing.TYPE_CHECKING: from .telegramclient import TelegramClient @@ -94,7 +95,7 @@ def _get_response_message(self: 'TelegramClient', request, result, input_chat): elif isinstance(update, ( _tl.UpdateNewChannelMessage, _tl.UpdateNewMessage)): - update.message._finish_init(self, entities, input_chat) + update.message = _custom.Message._new(self, update.message, entities, input_chat) # Pinning a message with `updatePinnedMessage` seems to # always produce a service message we can't map so return @@ -110,7 +111,7 @@ def _get_response_message(self: 'TelegramClient', request, result, input_chat): elif (isinstance(update, _tl.UpdateEditMessage) and helpers._entity_type(request.peer) != helpers._EntityType.CHANNEL): - update.message._finish_init(self, entities, input_chat) + update.message = _custom.Message._new(self, update.message, entities, input_chat) # Live locations use `sendMedia` but Telegram responds with # `updateEditMessage`, which means we won't have `id` field. @@ -123,28 +124,24 @@ def _get_response_message(self: 'TelegramClient', request, result, input_chat): 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 + return _custom.Message._new(self, update.message, entities, input_chat) elif isinstance(update, _tl.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 + id_to_message[update.message.id] = _custom.Message._new(self, update.message, entities, input_chat) elif isinstance(update, _tl.UpdateMessagePoll): if request.media.poll.id == update.poll_id: - m = _tl.Message( + return _custom.Message._new(self, _tl.Message( id=request.id, peer_id=utils.get_peer(request.peer), media=_tl.MessageMediaPoll( poll=update.poll, results=update.results ) - ) - m._finish_init(self, entities, input_chat) - return m + ), entities, input_chat) if request is None: return id_to_message diff --git a/telethon/_client/messages.py b/telethon/_client/messages.py index 5dae6eb4..3b63d35b 100644 --- a/telethon/_client/messages.py +++ b/telethon/_client/messages.py @@ -5,6 +5,7 @@ import warnings from .. import errors, hints, _tl from .._misc import helpers, utils, requestiter +from ..types import _custom _MAX_CHUNK_SIZE = 100 @@ -200,8 +201,7 @@ class _MessagesIter(requestiter.RequestIter): # is an attempt to avoid these duplicates, since the message # IDs are returned in descending order (or asc if reverse). self.last_id = message.id - message._finish_init(self.client, entities, self.entity) - self.buffer.append(message) + self.buffer.append(_custom.Message._new(self.client, message, entities, self.entity)) if len(r.messages) < self.request.limit: return True @@ -315,8 +315,7 @@ class _IDsIter(requestiter.RequestIter): from_id and message.peer_id != from_id): self.buffer.append(None) else: - message._finish_init(self.client, entities, self._entity) - self.buffer.append(message) + self.buffer.append(_custom.Message._new(self.client, message, entities, self._entity)) def iter_messages( @@ -498,7 +497,7 @@ async def send_message( result = await self(request) if isinstance(result, _tl.UpdateShortSentMessage): - message = _tl.Message( + return _custom.Message._new(self, _tl.Message( id=result.id, peer_id=await self._get_peer(entity), message=message, @@ -508,9 +507,7 @@ async def send_message( entities=result.entities, reply_markup=request.reply_markup, ttl_period=result.ttl_period - ) - message._finish_init(self, {}, entity) - return message + ), {}, entity) return self._get_response_message(request, result, entity) diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 85b28cd3..5c84a990 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -3783,6 +3783,9 @@ class TelegramClient: async def _handle_auto_reconnect(self: 'TelegramClient'): return await updates._handle_auto_reconnect(**locals()) + def _self_id(self: 'TelegramClient') -> typing.Optional[int]: + return users._self_id(**locals()) + # endregion Private # TODO re-patch everything to remove the intermediate calls diff --git a/telethon/events/album.py b/telethon/events/album.py index fdc3c02c..d8dacb98 100644 --- a/telethon/events/album.py +++ b/telethon/events/album.py @@ -168,8 +168,10 @@ class Album(EventBuilder): self._sender, self._input_sender = utils._get_entity_pair( self.sender_id, self._entities, client._entity_cache) - for msg in self.messages: - msg._finish_init(client, self._entities, None) + self.messages = [ + _custom.Message._new(client, m, self._entities, None) + for m in self.messages + ] if len(self.messages) == 1: # This will require hacks to be a proper album event diff --git a/telethon/events/chataction.py b/telethon/events/chataction.py index d330656a..75f19075 100644 --- a/telethon/events/chataction.py +++ b/telethon/events/chataction.py @@ -1,6 +1,7 @@ from .common import EventBuilder, EventCommon, name_inner_event from .._misc import utils from .. import _tl +from ..types import _custom @name_inner_event diff --git a/telethon/events/newmessage.py b/telethon/events/newmessage.py index cfe7b88a..58e1a425 100644 --- a/telethon/events/newmessage.py +++ b/telethon/events/newmessage.py @@ -3,6 +3,7 @@ import re from .common import EventBuilder, EventCommon, name_inner_event, _into_id_set from .._misc import utils from .. import _tl +from ..types import _custom @name_inner_event diff --git a/telethon/types/_custom/message.py b/telethon/types/_custom/message.py index 2be48847..88a1a615 100644 --- a/telethon/types/_custom/message.py +++ b/telethon/types/_custom/message.py @@ -9,9 +9,22 @@ from ..._misc import utils, tlobject from ... import errors, _tl +def _fwd(field, doc): + def fget(self): + try: + return self._message.__dict__[field] + except KeyError: + return None + + def fset(self, value): + self._message.__dict__[field] = value + + return property(fget, fset, None, doc) + + # TODO Figure out a way to have the code generator error on missing fields # Maybe parsing the init function alone if that's possible. -class Message(ChatGetter, SenderGetter, tlobject.TLObject): +class Message(ChatGetter, SenderGetter): """ This custom class aggregates both :tl:`Message` and :tl:`MessageService` to ease accessing their members. @@ -20,219 +33,192 @@ class Message(ChatGetter, SenderGetter, tlobject.TLObject): ` and `SenderGetter ` which means you have access to all their sender and chat properties and methods. - - Members: - out (`bool`): - Whether the message is outgoing (i.e. you sent it from - another session) or incoming (i.e. someone else sent it). - - Note that messages in your own chat are always incoming, - but this member will be `True` if you send a message - to your own chat. Messages you forward to your chat are - *not* considered outgoing, just like official clients - display them. - - mentioned (`bool`): - Whether you were mentioned in this message or not. - Note that replies to your own messages also count - as mentions. - - media_unread (`bool`): - Whether you have read the media in this message - or not, e.g. listened to the voice note media. - - silent (`bool`): - Whether the message should notify people with sound or not. - Previously used in channels, but since 9 August 2019, it can - also be `used in private chats - `_. - - post (`bool`): - Whether this message is a post in a broadcast - channel or not. - - from_scheduled (`bool`): - Whether this message was originated from a previously-scheduled - message or not. - - legacy (`bool`): - Whether this is a legacy message or not. - - edit_hide (`bool`): - Whether the edited mark of this message is edited - should be hidden (e.g. in GUI clients) or shown. - - pinned (`bool`): - Whether this message is currently pinned or not. - - id (`int`): - The ID of this message. This field is *always* present. - Any other member is optional and may be `None`. - - from_id (:tl:`Peer`): - The peer who sent this message, which is either - :tl:`PeerUser`, :tl:`PeerChat` or :tl:`PeerChannel`. - This value will be `None` for anonymous messages. - - peer_id (:tl:`Peer`): - The peer to which this message was sent, which is either - :tl:`PeerUser`, :tl:`PeerChat` or :tl:`PeerChannel`. This - will always be present except for empty messages. - - fwd_from (:tl:`MessageFwdHeader`): - The original forward header if this message is a forward. - You should probably use the `forward` property instead. - - via_bot_id (`int`): - The ID of the bot used to send this message - through its inline mode (e.g. "via @like"). - - reply_to (:tl:`MessageReplyHeader`): - The original reply header if this message is replying to another. - - date (`datetime`): - The UTC+0 `datetime` object indicating when this message - was sent. This will always be present except for empty - messages. - - message (`str`): - The string text of the message for `Message - ` instances, - which will be `None` for other types of messages. - - media (:tl:`MessageMedia`): - The media sent with this message if any (such as - photos, videos, documents, gifs, stickers, etc.). - - You may want to access the `photo`, `document` - etc. properties instead. - - If the media was not present or it was :tl:`MessageMediaEmpty`, - this member will instead be `None` for convenience. - - reply_markup (:tl:`ReplyMarkup`): - The reply markup for this message (which was sent - either via a bot or by a bot). You probably want - to access `buttons` instead. - - entities (List[:tl:`MessageEntity`]): - The list of markup entities in this message, - such as bold, italics, code, hyperlinks, etc. - - views (`int`): - The number of views this message from a broadcast - channel has. This is also present in forwards. - - forwards (`int`): - The number of times this message has been forwarded. - - replies (`int`): - The number of times another message has replied to this message. - - edit_date (`datetime`): - The date when this message was last edited. - - post_author (`str`): - The display name of the message sender to - show in messages sent to broadcast channels. - - grouped_id (`int`): - If this message belongs to a group of messages - (photo albums or video albums), all of them will - have the same value here. - - restriction_reason (List[:tl:`RestrictionReason`]) - An optional list of reasons why this message was restricted. - If the list is `None`, this message has not been restricted. - - ttl_period (`int`): - The Time To Live period configured for this message. - The message should be erased from wherever it's stored (memory, a - local database, etc.) when - ``datetime.now() > message.date + timedelta(seconds=message.ttl_period)``. - - action (:tl:`MessageAction`): - The message action object of the message for :tl:`MessageService` - instances, which will be `None` for other types of messages. """ + # region Forwarded properties + + out = _fwd('out', """ + Whether the message is outgoing (i.e. you sent it from + another session) or incoming (i.e. someone else sent it). + + Note that messages in your own chat are always incoming, + but this member will be `True` if you send a message + to your own chat. Messages you forward to your chat are + *not* considered outgoing, just like official clients + display them. + """) + + mentioned = _fwd('mentioned', """ + Whether you were mentioned in this message or not. + Note that replies to your own messages also count + as mentions. + """) + + media_unread = _fwd('media_unread', """ + Whether you have read the media in this message + or not, e.g. listened to the voice note media. + """) + + silent = _fwd('silent', """ + Whether the message should notify people with sound or not. + Previously used in channels, but since 9 August 2019, it can + also be `used in private chats + `_. + """) + + post = _fwd('post', """ + Whether this message is a post in a broadcast + channel or not. + """) + + from_scheduled = _fwd('from_scheduled', """ + Whether this message was originated from a previously-scheduled + message or not. + """) + + legacy = _fwd('legacy', """ + Whether this is a legacy message or not. + """) + + edit_hide = _fwd('edit_hide', """ + Whether the edited mark of this message is edited + should be hidden (e.g. in GUI clients) or shown. + """) + + pinned = _fwd('pinned', """ + Whether this message is currently pinned or not. + """) + + id = _fwd('id', """ + The ID of this message. This field is *always* present. + Any other member is optional and may be `None`. + """) + + from_id = _fwd('from_id', """ + The peer who sent this message, which is either + :tl:`PeerUser`, :tl:`PeerChat` or :tl:`PeerChannel`. + This value will be `None` for anonymous messages. + """) + + peer_id = _fwd('peer_id', """ + The peer to which this message was sent, which is either + :tl:`PeerUser`, :tl:`PeerChat` or :tl:`PeerChannel`. This + will always be present except for empty messages. + """) + + fwd_from = _fwd('fwd_from', """ + The original forward header if this message is a forward. + You should probably use the `forward` property instead. + """) + + via_bot_id = _fwd('via_bot_id', """ + The ID of the bot used to send this message + through its inline mode (e.g. "via @like"). + """) + + reply_to = _fwd('reply_to', """ + The original reply header if this message is replying to another. + """) + + date = _fwd('date', """ + The UTC+0 `datetime` object indicating when this message + was sent. This will always be present except for empty + messages. + """) + + message = _fwd('message', """ + The string text of the message for `Message + ` instances, + which will be `None` for other types of messages. + """) + + @property + def media(self): + """ + The media sent with this message if any (such as + photos, videos, documents, gifs, stickers, etc.). + + You may want to access the `photo`, `document` + etc. properties instead. + + If the media was not present or it was :tl:`MessageMediaEmpty`, + this member will instead be `None` for convenience. + """ + try: + media = self._message.media + except AttributeError: + return None + + return None if media.CONSTRUCTOR_ID == 0x3ded6320 else media + + @media.setter + def media(self, value): + self._message.media = value + + reply_markup = _fwd('reply_markup', """ + The reply markup for this message (which was sent + either via a bot or by a bot). You probably want + to access `buttons` instead. + """) + + entities = _fwd('entities', """ + The list of markup entities in this message, + such as bold, italics, code, hyperlinks, etc. + """) + + views = _fwd('views', """ + The number of views this message from a broadcast + channel has. This is also present in forwards. + """) + + forwards = _fwd('forwards', """ + The number of times this message has been forwarded. + """) + + replies = _fwd('replies', """ + The number of times another message has replied to this message. + """) + + edit_date = _fwd('edit_date', """ + The date when this message was last edited. + """) + + post_author = _fwd('post_author', """ + The display name of the message sender to + show in messages sent to broadcast channels. + """) + + grouped_id = _fwd('grouped_id', """ + If this message belongs to a group of messages + (photo albums or video albums), all of them will + have the same value here. + + restriction_reason (List[:tl:`RestrictionReason`]) + An optional list of reasons why this message was restricted. + If the list is `None`, this message has not been restricted. + """) + + ttl_period = _fwd('ttl_period', """ + The Time To Live period configured for this message. + The message should be erased from wherever it's stored (memory, a + local database, etc.) when + ``datetime.now() > message.date + timedelta(seconds=message.ttl_period)``. + """) + + action = _fwd('action', """ + The message action object of the message for :tl:`MessageService` + instances, which will be `None` for other types of messages. + """) + + # endregion + # region Initialization - def __init__( - # Common to all - self, id: int, - - # Common to Message and MessageService (mandatory) - peer_id: _tl.TypePeer = None, - date: Optional[datetime] = None, - - # Common to Message and MessageService (flags) - out: Optional[bool] = None, - mentioned: Optional[bool] = None, - media_unread: Optional[bool] = None, - silent: Optional[bool] = None, - post: Optional[bool] = None, - from_id: Optional[_tl.TypePeer] = None, - reply_to: Optional[_tl.TypeMessageReplyHeader] = None, - ttl_period: Optional[int] = None, - - # For Message (mandatory) - message: Optional[str] = None, - - # For Message (flags) - fwd_from: Optional[_tl.TypeMessageFwdHeader] = None, - via_bot_id: Optional[int] = None, - media: Optional[_tl.TypeMessageMedia] = None, - reply_markup: Optional[_tl.TypeReplyMarkup] = None, - entities: Optional[List[_tl.TypeMessageEntity]] = None, - views: Optional[int] = None, - edit_date: Optional[datetime] = None, - post_author: Optional[str] = None, - grouped_id: Optional[int] = None, - from_scheduled: Optional[bool] = None, - legacy: Optional[bool] = None, - edit_hide: Optional[bool] = None, - pinned: Optional[bool] = None, - restriction_reason: Optional[_tl.TypeRestrictionReason] = None, - forwards: Optional[int] = None, - replies: Optional[_tl.TypeMessageReplies] = None, - - # For MessageAction (mandatory) - action: Optional[_tl.TypeMessageAction] = None - ): - # Common properties to messages, then to service (in the order they're defined in the `.tl`) - self.out = bool(out) - self.mentioned = mentioned - self.media_unread = media_unread - self.silent = silent - self.post = post - self.from_scheduled = from_scheduled - self.legacy = legacy - self.edit_hide = edit_hide - self.id = id - self.from_id = from_id - self.peer_id = peer_id - self.fwd_from = fwd_from - self.via_bot_id = via_bot_id - self.reply_to = reply_to - self.date = date - self.message = message - self.media = None if isinstance(media, _tl.MessageMediaEmpty) else media - self.reply_markup = reply_markup - self.entities = entities - self.views = views - self.forwards = forwards - self.replies = replies - self.edit_date = edit_date - self.pinned = pinned - self.post_author = post_author - self.grouped_id = grouped_id - self.restriction_reason = restriction_reason - self.ttl_period = ttl_period - self.action = action + def __init__(self, client, message): + self._client = client + self._message = message # Convenient storage for custom functions - # TODO This is becoming a bit of bloat self._client = None self._text = None self._file = None @@ -246,28 +232,25 @@ class Message(ChatGetter, SenderGetter, tlobject.TLObject): self._linked_chat = None sender_id = None - if from_id is not None: - sender_id = utils.get_peer_id(from_id) - elif peer_id: + if self.from_id is not None: + sender_id = utils.get_peer_id(self.from_id) + elif self.peer_id: # If the message comes from a Channel, let the sender be it # ...or... # incoming messages in private conversations no longer have from_id # (layer 119+), but the sender can only be the chat we're in. - if post or (not out and isinstance(peer_id, _tl.PeerUser)): - sender_id = utils.get_peer_id(peer_id) + if self.post or (not self.out and isinstance(self.peer_id, _tl.PeerUser)): + sender_id = utils.get_peer_id(self.peer_id) # Note that these calls would reset the client - ChatGetter.__init__(self, peer_id, broadcast=post) + ChatGetter.__init__(self, self.peer_id, broadcast=self.post) SenderGetter.__init__(self, sender_id) self._forward = None - def _finish_init(self, client, entities, input_chat): - """ - Finishes the initialization of this message by setting - the client that sent the message and making use of the - known entities. - """ + @classmethod + def _new(cls, client, message, entities, input_chat): + self = cls(client, message) self._client = client # Make messages sent to ourselves outgoing unless they're forwarded. @@ -314,6 +297,7 @@ class Message(ChatGetter, SenderGetter, tlobject.TLObject): self._linked_chat = entities.get(utils.get_peer_id( _tl.PeerChannel(self.replies.channel_id))) + return self # endregion Initialization From eb659b9a581172c3444b096fbe4b334c630b722f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 13 Sep 2021 20:43:44 +0200 Subject: [PATCH 052/256] Fix _write_all_tlobjects call --- telethon_generator/generators/tlobject.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/telethon_generator/generators/tlobject.py b/telethon_generator/generators/tlobject.py index 78a09884..9cb43140 100644 --- a/telethon_generator/generators/tlobject.py +++ b/telethon_generator/generators/tlobject.py @@ -52,7 +52,7 @@ BASE_TYPES = ('string', 'bytes', 'int', 'long', 'int128', def _write_modules( - out_dir, in_mod, kind, namespace_tlobjects, type_constructors, layer): + out_dir, in_mod, kind, namespace_tlobjects, type_constructors, layer, all_tlobjects): # namespace_tlobjects: {'namespace', [TLObject]} out_dir.mkdir(parents=True, exist_ok=True) for ns, tlobjects in namespace_tlobjects.items(): @@ -164,7 +164,7 @@ def _write_modules( builder.writeln(line) if not ns and kind == 'TLObject': - _write_all_tlobjects(tlobjects, layer, builder) + _write_all_tlobjects(all_tlobjects, layer, builder) def _write_source_code(tlobject, kind, builder, type_constructors): @@ -699,9 +699,9 @@ def generate_tlobjects(tlobjects, layer, input_mod, output_dir): type_constructors[tlobject.result].append(tlobject) _write_modules(output_dir, input_mod, 'TLObject', - namespace_types, type_constructors, layer) + namespace_types, type_constructors, layer, tlobjects) _write_modules(output_dir / 'fn', input_mod + '.fn', 'TLRequest', - namespace_functions, type_constructors, layer) + namespace_functions, type_constructors, layer, tlobjects) def clean_tlobjects(output_dir): From 943ad892f7a2926b4bf2f851992155d343cb8247 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 13 Sep 2021 21:00:31 +0200 Subject: [PATCH 053/256] Address remaining uses of the Request suffix with raw API --- telethon/_client/chats.py | 2 +- telethon/_client/messages.py | 4 ++-- telethon/_client/telegrambaseclient.py | 4 ++-- telethon/_client/telegramclient.py | 8 +++---- telethon/_client/updates.py | 2 +- telethon/_client/uploads.py | 2 +- telethon/_client/users.py | 2 +- telethon/_crypto/cdndecrypter.py | 2 +- telethon/_network/mtprotostate.py | 2 +- telethon/events/callbackquery.py | 2 +- telethon/types/_custom/draft.py | 2 +- telethon/types/_custom/messagebutton.py | 2 +- telethon_examples/payment.py | 30 ++++++++++++------------- 13 files changed, 32 insertions(+), 32 deletions(-) diff --git a/telethon/_client/chats.py b/telethon/_client/chats.py index 7eb6a2a1..21fb02e5 100644 --- a/telethon/_client/chats.py +++ b/telethon/_client/chats.py @@ -524,7 +524,7 @@ def action( raise ValueError('Cannot use {} as action'.format(action)) if isinstance(action, _tl.SendMessageCancelAction): - # ``SetTypingRequest.resolve`` will get input peer of ``entity``. + # ``SetTyping.resolve`` will get input peer of ``entity``. return self(_tl.fn.messages.SetTyping( entity, _tl.SendMessageCancelAction())) diff --git a/telethon/_client/messages.py b/telethon/_client/messages.py index 3b63d35b..b7bbf518 100644 --- a/telethon/_client/messages.py +++ b/telethon/_client/messages.py @@ -244,7 +244,7 @@ class _MessagesIter(requestiter.RequestIter): # We want to skip the one we already have self.request.offset_id += 1 - if isinstance(self.request, _tl.fn.messages.SearchRequest): + if isinstance(self.request, _tl.fn.messages.Search): # Unlike getHistory and searchGlobal that use *offset* date, # this is *max* date. This means that doing a search in reverse # will break it. Since it's not really needed once we're going @@ -254,7 +254,7 @@ class _MessagesIter(requestiter.RequestIter): # getHistory, searchGlobal and getReplies call it offset_date self.request.offset_date = last_message.date - if isinstance(self.request, _tl.fn.messages.SearchGlobalRequest): + if isinstance(self.request, _tl.fn.messages.SearchGlobal): if last_message.input_chat: self.request.offset_peer = last_message.input_chat else: diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 0ac127f0..d8d333f0 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -197,7 +197,7 @@ def init( _tl.InputClientProxy(*connection.address_info(proxy)) # Used on connection. Capture the variables in a lambda since - # exporting clients need to create this InvokeWithLayerRequest. + # exporting clients need to create this InvokeWithLayer. system = platform.uname() if system.machine in ('x86_64', 'AMD64'): @@ -559,7 +559,7 @@ async def _get_cdn_client(self: 'TelegramClient', cdn_redirect): # This will make use of the new RSA keys for this specific CDN. # - # We won't be calling GetConfigRequest because it's only called + # We won't be calling GetConfig because it's only called # when needed by ._get_dc, and also it's static so it's likely # set already. Avoid invoking non-CDN methods by not syncing updates. client.connect(_sync_updates=False) diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 5c84a990..1a12b122 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -173,7 +173,7 @@ class TelegramClient: Returns a :ref:`telethon-client` which calls methods behind a takeout session. It does so by creating a proxy object over the current client through - which making requests will use :tl:`InvokeWithTakeoutRequest` to wrap + which making requests will use :tl:`InvokeWithTakeout` to wrap them. In other words, returns the current client modified so that requests are done as a takeout: @@ -764,7 +764,7 @@ class TelegramClient: This has no effect if a ``filter`` is given. Yields - The :tl:`User` objects returned by :tl:`GetParticipantsRequest` + The :tl:`User` objects returned by :tl:`GetParticipants` with an additional ``.participant`` attribute which is the matched :tl:`ChannelParticipant` type for channels/megagroups or :tl:`ChatParticipants` for normal chats. @@ -2067,7 +2067,7 @@ class TelegramClient: .. note:: - Telegram's flood wait limit for :tl:`GetHistoryRequest` seems to + Telegram's flood wait limit for :tl:`GetHistory` seems to be around 30 seconds per 10 requests, therefore a sleep of 1 second is the default for this limit (or above). @@ -2125,7 +2125,7 @@ class TelegramClient: wait_time (`int`): Wait time (in seconds) between different - :tl:`GetHistoryRequest`. Use this parameter to avoid hitting + :tl:`GetHistory`. Use this parameter to avoid hitting the ``FloodWaitError`` as needed. If left to `None`, it will default to 1 second only if the limit is higher than 3000. diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index 9d1e205c..93e8a9bc 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -257,7 +257,7 @@ async def _dispatch_update(self: 'TelegramClient', update, others, channel_id, p # we should be okay (no flood waits) even if more occur. pass except ValueError: - # There is a chance that GetFullChannelRequest and GetDifferenceRequest + # There is a chance that GetFullChannel and GetDifference # inside the _get_difference() function will end up with # ValueError("Request was unsuccessful N time(s)") for whatever reasons. pass diff --git a/telethon/_client/uploads.py b/telethon/_client/uploads.py index fb5bfc0c..244c5b59 100644 --- a/telethon/_client/uploads.py +++ b/telethon/_client/uploads.py @@ -341,7 +341,7 @@ async def upload_file( # what Telegram wants. hash_md5.update(part) - # The SavePartRequest is different depending on whether + # The SavePart is different depending on whether # the file is too large or not (over or less than 10MB) if is_big: request = _tl.fn.upload.SaveBigFilePart( diff --git a/telethon/_client/users.py b/telethon/_client/users.py index 9bf18faf..f3393196 100644 --- a/telethon/_client/users.py +++ b/telethon/_client/users.py @@ -208,7 +208,7 @@ async def get_entity( chats = lists[helpers._EntityType.CHAT] channels = lists[helpers._EntityType.CHANNEL] if users: - # GetUsersRequest has a limit of 200 per call + # GetUsers has a limit of 200 per call tmp = [] while users: curr, users = users[:200], users[200:] diff --git a/telethon/_crypto/cdndecrypter.py b/telethon/_crypto/cdndecrypter.py index 8347a561..73a568d1 100644 --- a/telethon/_crypto/cdndecrypter.py +++ b/telethon/_crypto/cdndecrypter.py @@ -74,7 +74,7 @@ class CdnDecrypter: def get_file(self): """ - Calls GetCdnFileRequest and decrypts its bytes. + Calls GetCdnFile and decrypts its bytes. Also ensures that the file hasn't been tampered. :return: the CdnFile result. diff --git a/telethon/_network/mtprotostate.py b/telethon/_network/mtprotostate.py index ec777d75..f96554ac 100644 --- a/telethon/_network/mtprotostate.py +++ b/telethon/_network/mtprotostate.py @@ -102,7 +102,7 @@ class MTProtoState: # The `RequestState` stores `bytes(request)`, not the request itself. # `invokeAfterMsg` wants a `TLRequest` though, hence the wrapping. body = GzipPacked.gzip_if_smaller(content_related, - bytes(_tl.fn.InvokeAfterMsgRequest(after_id, _OpaqueRequest(data)))) + bytes(_tl.fn.InvokeAfterMsg(after_id, _OpaqueRequest(data)))) buffer.write(struct.pack(' types.InputMediaInvoice: - price = types.LabeledPrice(label=price_label, amount=price_amount) # label - just a text, amount=10000 means 100.00 - invoice = types.Invoice( + description: str, payload: str, start_param: str) -> _tl.InputMediaInvoice: + price = _tl.LabeledPrice(label=price_label, amount=price_amount) # label - just a text, amount=10000 means 100.00 + invoice = _tl.Invoice( currency=currency, # currency like USD prices=[price], # there could be a couple of prices. test=True, # if you're working with test token, else set test=False. @@ -114,14 +114,14 @@ def generate_invoice(price_label: str, price_amount: int, currency: str, title: phone_to_provider=False, email_to_provider=False ) - return types.InputMediaInvoice( + return _tl.InputMediaInvoice( title=title, description=description, invoice=invoice, payload=payload.encode('UTF-8'), # payload, which will be sent to next 2 handlers provider=provider_token, - provider_data=types.DataJSON('{}'), + provider_data=_tl.DataJSON('{}'), # data about the invoice, which will be shared with the payment provider. A detailed description of # required fields should be provided by the payment provider. From e9f9994f4ae25b0fbc900b395c8296215cbd7334 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 17 Sep 2021 19:35:10 +0200 Subject: [PATCH 054/256] Unify client.iter_* methods --- readthedocs/misc/v2-migration-guide.rst | 34 +++++ telethon/_client/chats.py | 24 +--- telethon/_client/dialogs.py | 16 +-- telethon/_client/messages.py | 21 +-- telethon/_client/telegramclient.py | 168 +++++------------------- telethon/_misc/requestiter.py | 3 + 6 files changed, 78 insertions(+), 188 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 483c5287..e48fb41a 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -50,6 +50,40 @@ removed. This implies: // TODO provide standalone alternative for this? +The "iter" variant of the client methods have been removed +---------------------------------------------------------- + +Instead, you can now use the result of the ``get_*`` variant. For instance, where before you had: + +.. code-block:: python + + async for message in client.iter_messages(...): + pass + +You would now do: + + .. code-block:: python + + async for message in client.get_messages(...): + pass # ^^^ now it's get, not iter + +You can still use ``await`` on the ``get_`` methods to retrieve the list. + +The removed methods are: + +* iter_messages +* iter_dialogs +* iter_participants +* iter_admin_log +* iter_profile_photos +* iter_drafts + +The only exception to this rule is ``iter_download``. + +// TODO keep providing the old ``iter_`` versions? it doesn't really hurt, even if the recommended way changed +// TODO does the download really need to be special? get download is kind of weird though + + Raw API methods have been renamed and are now considered private ---------------------------------------------------------------- diff --git a/telethon/_client/chats.py b/telethon/_client/chats.py index 21fb02e5..ec786b36 100644 --- a/telethon/_client/chats.py +++ b/telethon/_client/chats.py @@ -401,7 +401,7 @@ class _ProfilePhotoIter(requestiter.RequestIter): self.request.offset_id = result.messages[-1].id -def iter_participants( +def get_participants( self: 'TelegramClient', entity: 'hints.EntityLike', limit: float = None, @@ -418,14 +418,8 @@ def iter_participants( aggressive=aggressive ) -async def get_participants( - self: 'TelegramClient', - *args, - **kwargs) -> 'hints.TotalList': - return await self.iter_participants(*args, **kwargs).collect() - -def iter_admin_log( +def get_admin_log( self: 'TelegramClient', entity: 'hints.EntityLike', limit: float = None, @@ -474,14 +468,8 @@ def iter_admin_log( group_call=group_call ) -async def get_admin_log( - self: 'TelegramClient', - *args, - **kwargs) -> 'hints.TotalList': - return await self.iter_admin_log(*args, **kwargs).collect() - -def iter_profile_photos( +def get_profile_photos( self: 'TelegramClient', entity: 'hints.EntityLike', limit: int = None, @@ -496,12 +484,6 @@ def iter_profile_photos( max_id=max_id ) -async def get_profile_photos( - self: 'TelegramClient', - *args, - **kwargs) -> 'hints.TotalList': - return await self.iter_profile_photos(*args, **kwargs).collect() - def action( self: 'TelegramClient', diff --git a/telethon/_client/dialogs.py b/telethon/_client/dialogs.py index b293edca..d4b4644b 100644 --- a/telethon/_client/dialogs.py +++ b/telethon/_client/dialogs.py @@ -136,7 +136,7 @@ class _DraftsIter(requestiter.RequestIter): return [] -def iter_dialogs( +def get_dialogs( self: 'TelegramClient', limit: float = None, *, @@ -162,11 +162,8 @@ def iter_dialogs( folder=folder ) -async def get_dialogs(self: 'TelegramClient', *args, **kwargs) -> 'hints.TotalList': - return await self.iter_dialogs(*args, **kwargs).collect() - -def iter_drafts( +def get_drafts( self: 'TelegramClient', entity: 'hints.EntitiesLike' = None ) -> _DraftsIter: @@ -176,15 +173,6 @@ def iter_drafts( # 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': - 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', diff --git a/telethon/_client/messages.py b/telethon/_client/messages.py index b7bbf518..c29610e4 100644 --- a/telethon/_client/messages.py +++ b/telethon/_client/messages.py @@ -318,7 +318,7 @@ class _IDsIter(requestiter.RequestIter): self.buffer.append(_custom.Message._new(self.client, message, entities, self._entity)) -def iter_messages( +def get_messages( self: 'TelegramClient', entity: 'hints.EntityLike', limit: float = None, @@ -368,25 +368,6 @@ def iter_messages( scheduled=scheduled ) -async def get_messages(self: 'TelegramClient', *args, **kwargs) -> 'hints.TotalList': - if len(args) == 1 and 'limit' not in kwargs: - if 'min_id' in kwargs and 'max_id' in kwargs: - kwargs['limit'] = None - else: - kwargs['limit'] = 1 - - it = self.iter_messages(*args, **kwargs) - - ids = kwargs.get('ids') - if ids and not utils.is_list_like(ids): - async for message in it: - return message - else: - # Iterator exhausted = empty, to handle InputMessageReplyTo - return None - - return await it.collect() - async def _get_comment_data( self: 'TelegramClient', diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 1a12b122..1482dbdd 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -716,7 +716,7 @@ class TelegramClient: # region Chats - def iter_participants( + def get_participants( self: 'TelegramClient', entity: 'hints.EntityLike', limit: float = None, @@ -784,32 +784,14 @@ class TelegramClient: from telethon.tl.types import ChannelParticipantsAdmins async for user in client.iter_participants(chat, filter=ChannelParticipantsAdmins): print(user.first_name) + + # Get a list of 0 people but print the total amount of participants in the chat + users = await client.get_participants(chat, limit=0) + print(users.total) """ - return chats.iter_participants(**locals()) + return chats.get_participants(**locals()) - async def get_participants( - self: 'TelegramClient', - *args, - **kwargs) -> 'hints.TotalList': - """ - Same as `iter_participants()`, but returns a - `TotalList ` instead. - - Example - .. code-block:: python - - users = await client.get_participants(chat) - print(users[0].first_name) - - for user in users: - if user.username is not None: - print(user.username) - """ - return await chats.get_participants(*args, **kwargs) - - get_participants.__signature__ = inspect.signature(iter_participants) - - def iter_admin_log( + def get_admin_log( self: 'TelegramClient', entity: 'hints.EntityLike', limit: float = None, @@ -931,30 +913,16 @@ class TelegramClient: async for event in client.iter_admin_log(channel): if event.changed_title: print('The title changed from', event.old, 'to', event.new) - """ - return chats.iter_admin_log(**locals()) - async def get_admin_log( - self: 'TelegramClient', - *args, - **kwargs) -> 'hints.TotalList': - """ - Same as `iter_admin_log()`, but returns a ``list`` instead. - - Example - .. code-block:: python - - # Get a list of deleted message events which said "heck" - events = await client.get_admin_log(channel, search='heck', delete=True) + # Get all events of deleted message events which said "heck" and print the last one + events = await client.get_admin_log(channel, limit=None, search='heck', delete=True) # Print the old message before it was deleted - print(events[0].old) + print(events[-1].old) """ - return await chats.get_admin_log(*args, **kwargs) + return chats.get_admin_log(**locals()) - get_admin_log.__signature__ = inspect.signature(iter_admin_log) - - def iter_profile_photos( + def get_profile_photos( self: 'TelegramClient', entity: 'hints.EntityLike', limit: int = None, @@ -991,29 +959,12 @@ class TelegramClient: # Download all the profile photos of some user async for photo in client.iter_profile_photos(user): await client.download_media(photo) - """ - return chats.iter_profile_photos(**locals()) - async def get_profile_photos( - self: 'TelegramClient', - *args, - **kwargs) -> 'hints.TotalList': - """ - Same as `iter_profile_photos()`, but returns a - `TotalList ` instead. - - Example - .. code-block:: python - - # Get the photos of a channel - photos = await client.get_profile_photos(channel) - - # Download the oldest photo + # Get all the photos of a channel and download the oldest one + photos = await client.get_profile_photos(channel, limit=None) await client.download_media(photos[-1]) """ - return await chats.get_profile_photos(*args, **kwargs) - - get_profile_photos.__signature__ = inspect.signature(iter_profile_photos) + return chats.get_profile_photos(**locals()) def action( self: 'TelegramClient', @@ -1443,7 +1394,7 @@ class TelegramClient: # region Dialogs - def iter_dialogs( + def get_dialogs( self: 'TelegramClient', limit: float = None, *, @@ -1517,19 +1468,9 @@ class TelegramClient: # Print all dialog IDs and the title, nicely formatted async for dialog in client.iter_dialogs(): print('{:>14}: {}'.format(dialog.id, dialog.title)) - """ - return dialogs.iter_dialogs(**locals()) - - async def get_dialogs(self: 'TelegramClient', *args, **kwargs) -> 'hints.TotalList': - """ - Same as `iter_dialogs()`, but returns a - `TotalList ` instead. - - Example - .. code-block:: python # Get all open conversation, print the title of the first - dialogs = await client.get_dialogs() + dialogs = await client.get_dialogs(limit=None) first = dialogs[0] print(first.title) @@ -1537,18 +1478,16 @@ class TelegramClient: 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) + non_archived = await client.get_dialogs(folder=0, limit=None) + non_archived = await client.get_dialogs(archived=False, limit=None) # Getting only archived dialogs (both equivalent) - archived = await client.get_dialogs(folder=1) - archived = await client.get_dialogs(archived=True) + archived = await client.get_dialogs(folder=1, limit=None) + archived = await client.get_dialogs(archived=True, limit=None) """ - return await dialogs.get_dialogs(*args, **kwargs) + return dialogs.get_dialogs(**locals()) - get_dialogs.__signature__ = inspect.signature(iter_dialogs) - - def iter_drafts( + def get_drafts( self: 'TelegramClient', entity: 'hints.EntitiesLike' = None ) -> dialogs._DraftsIter: @@ -1575,28 +1514,12 @@ class TelegramClient: # Getting the drafts with 'bot1' and 'bot2' async for draft in client.iter_drafts(['bot1', 'bot2']): print(draft.text) - """ - return dialogs.iter_drafts(**locals()) - - 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) + print(draft.text) """ - return await dialogs.get_drafts(**locals()) + return dialogs.get_drafts(**locals()) async def edit_folder( self: 'TelegramClient', @@ -2037,7 +1960,7 @@ class TelegramClient: # region Messages - def iter_messages( + def get_messages( self: 'TelegramClient', entity: 'hints.EntityLike', limit: float = None, @@ -2199,8 +2122,8 @@ class TelegramClient: async for message in client.iter_messages(chat, reverse=True): print(message.id, message.text) - # Filter by sender - async for message in client.iter_messages(chat, from_user='me'): + # Filter by sender, and limit to 10 + async for message in client.iter_messages(chat, 10, from_user='me'): print(message.text) # Server-side search with fuzzy text @@ -2215,43 +2138,22 @@ class TelegramClient: # Getting comments from a post in a channel: async for message in client.iter_messages(channel, reply_to=123): print(message.chat.title, message.text) - """ - return messages.iter_messages(**locals()) - - async def get_messages(self: 'TelegramClient', *args, **kwargs) -> 'hints.TotalList': - """ - Same as `iter_messages()`, but returns a - `TotalList ` instead. - - If the `limit` is not set, it will be 1 by default unless both - `min_id` **and** `max_id` are set (as *named* arguments), in - which case the entire range will be returned. - - This is so because any integer limit would be rather arbitrary and - it's common to only want to fetch one message, but if a range is - specified it makes sense that it should return the entirety of it. - - If `ids` is present in the *named* arguments and is not a list, - a single `Message ` will be - returned for convenience instead of a list. - - Example - .. code-block:: python # Get 0 photos and print the total to show how many photos there are from telethon.tl.types import InputMessagesFilterPhotos photos = await client.get_messages(chat, 0, filter=InputMessagesFilterPhotos) print(photos.total) - # Get all the photos - photos = await client.get_messages(chat, None, filter=InputMessagesFilterPhotos) + # Get all the photos in a list + all_photos = await client.get_messages(chat, None, filter=InputMessagesFilterPhotos) - # Get messages by ID: + # Get the last photo or None if none has been sent yet (same as setting limit 1) + photo = await client.get_messages(chat, filter=InputMessagesFilterPhotos) + + # Get a single message given an ID: message_1337 = await client.get_messages(chat, ids=1337) """ - return await messages.get_messages(**locals()) - - get_messages.__signature__ = inspect.signature(iter_messages) + return messages.get_messages(**locals()) async def send_message( self: 'TelegramClient', diff --git a/telethon/_misc/requestiter.py b/telethon/_misc/requestiter.py index 6473fe0f..9c837c05 100644 --- a/telethon/_misc/requestiter.py +++ b/telethon/_misc/requestiter.py @@ -114,3 +114,6 @@ class RequestIter(abc.ABC): def __reversed__(self): self.reverse = not self.reverse return self # __aiter__ will be called after, too + + def __await__(self): + return self.collect().__await__() From be3ed894c6a157404204683bcda630eab5584c25 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 17 Sep 2021 20:04:57 +0200 Subject: [PATCH 055/256] Make changes to the default limit in client.get_list methods --- readthedocs/misc/v2-migration-guide.rst | 25 +++++++++++++++++++++++++ telethon/_client/dialogs.py | 10 ++++++---- telethon/_client/telegramclient.py | 10 +++++----- telethon/_misc/requestiter.py | 16 +++++++++++++--- 4 files changed, 49 insertions(+), 12 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index e48fb41a..cf5fce04 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -80,6 +80,31 @@ The removed methods are: The only exception to this rule is ``iter_download``. +Additionally, when using ``await``, if the method was called with a limit of 1 (either through +setting just one value to fetch, or setting the limit to one), either ``None`` or a single item +(outside of a ``list``) will be returned. This used to be the case only for ``get_messages``, +but now all methods behave in the same way for consistency. + +When using ``async for``, the default limit will be ``None``, meaning all items will be fetched. +When using ``await``, the default limit will be ``1``, meaning the latest item will be fetched. +If you want to use ``await`` but still get a list, use the ``.collect()`` method to collect the +results into a list: + +.. code-block:: python + + chat = ... + + # will iterate over all (default limit=None) + async for message in client.get_messages(chat): + ... + + # will return either a single Message or None if there is not any (limit=1) + message = await client.get_messages(chat) + + # will collect all messages into a list (default limit=None). will also take long! + all_messages = await client.get_messages(chat).collect() + + // TODO keep providing the old ``iter_`` versions? it doesn't really hurt, even if the recommended way changed // TODO does the download really need to be special? get download is kind of weird though diff --git a/telethon/_client/dialogs.py b/telethon/_client/dialogs.py index d4b4644b..1277a3a4 100644 --- a/telethon/_client/dialogs.py +++ b/telethon/_client/dialogs.py @@ -167,11 +167,13 @@ def get_drafts( self: 'TelegramClient', entity: 'hints.EntitiesLike' = None ) -> _DraftsIter: - if entity and not utils.is_list_like(entity): - entity = (entity,) + limit = None + if entity: + if not utils.is_list_like(entity): + entity = (entity,) + limit = len(entity) - # TODO Passing a limit here makes no sense - return _DraftsIter(self, None, entities=entity) + return _DraftsIter(self, limit, entities=entity) async def edit_folder( diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 1482dbdd..1501fee3 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -719,7 +719,7 @@ class TelegramClient: def get_participants( self: 'TelegramClient', entity: 'hints.EntityLike', - limit: float = None, + limit: float = (), *, search: str = '', filter: '_tl.TypeChannelParticipantsFilter' = None, @@ -794,7 +794,7 @@ class TelegramClient: def get_admin_log( self: 'TelegramClient', entity: 'hints.EntityLike', - limit: float = None, + limit: float = (), *, max_id: int = 0, min_id: int = 0, @@ -925,7 +925,7 @@ class TelegramClient: def get_profile_photos( self: 'TelegramClient', entity: 'hints.EntityLike', - limit: int = None, + limit: int = (), *, offset: int = 0, max_id: int = 0) -> chats._ProfilePhotoIter: @@ -1396,7 +1396,7 @@ class TelegramClient: def get_dialogs( self: 'TelegramClient', - limit: float = None, + limit: float = (), *, offset_date: 'hints.DateLike' = None, offset_id: int = 0, @@ -1963,7 +1963,7 @@ class TelegramClient: def get_messages( self: 'TelegramClient', entity: 'hints.EntityLike', - limit: float = None, + limit: float = (), *, offset_date: 'hints.DateLike' = None, offset_id: int = 0, diff --git a/telethon/_misc/requestiter.py b/telethon/_misc/requestiter.py index 9c837c05..96ceb97a 100644 --- a/telethon/_misc/requestiter.py +++ b/telethon/_misc/requestiter.py @@ -28,12 +28,13 @@ class RequestIter(abc.ABC): self.reverse = reverse self.wait_time = wait_time self.kwargs = kwargs - self.limit = max(float('inf') if limit is None else limit, 0) + self.limit = max(float('inf') if limit is None or limit == () else limit, 0) self.left = self.limit self.buffer = None self.index = 0 self.total = None self.last_load = 0 + self.return_single = limit == 1 or limit == () async def _init(self, **kwargs): """ @@ -86,11 +87,20 @@ class RequestIter(abc.ABC): self.left = self.limit return self - async def collect(self): + async def collect(self, force_list=True): """ Create a `self` iterator and collect it into a `TotalList` (a normal list with a `.total` attribute). + + If ``force_list`` is ``False`` and ``self.return_single`` is ``True``, no list + will be returned. Instead, either a single item or ``None`` will be returned. """ + if not force_list and self.return_single: + self.limit = 1 + async for message in self: + return message + return None + result = helpers.TotalList() async for message in self: result.append(message) @@ -116,4 +126,4 @@ class RequestIter(abc.ABC): return self # __aiter__ will be called after, too def __await__(self): - return self.collect().__await__() + return self.collect(force_list=False).__await__() From 1036c3cb525d23d60c26c53df7b125478abefb52 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 17 Sep 2021 20:13:05 +0200 Subject: [PATCH 056/256] Remove the aggressive hack from get_participants --- readthedocs/misc/v2-migration-guide.rst | 7 ++ telethon/_client/chats.py | 99 ++++++++++--------------- telethon/_client/telegramclient.py | 16 +--- 3 files changed, 46 insertions(+), 76 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index cf5fce04..628b7a95 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -218,6 +218,13 @@ your handlers much more easily. // TODO provide standalone alternative for this? +The aggressive parameter hack has been removed +---------------------------------------------- + +The "aggressive" hack in ``get_participants`` (and ``iter_participants``) is now gone. +It was not reliable, and was a cause of flood wait errors. + + The TelegramClient is no longer made out of mixins -------------------------------------------------- diff --git a/telethon/_client/chats.py b/telethon/_client/chats.py index ec786b36..3fcce1b4 100644 --- a/telethon/_client/chats.py +++ b/telethon/_client/chats.py @@ -94,7 +94,7 @@ class _ChatAction: class _ParticipantsIter(requestiter.RequestIter): - async def _init(self, entity, filter, search, aggressive): + async def _init(self, entity, filter, search): if isinstance(filter, type): if filter in (_tl.ChannelParticipantsBanned, _tl.ChannelParticipantsKicked, @@ -118,9 +118,6 @@ class _ParticipantsIter(requestiter.RequestIter): else: self.filter_entity = lambda ent: True - # Only used for channels, but we should always set the attribute - self.requests = [] - if ty == helpers._EntityType.CHANNEL: if self.limit <= 0: # May not have access to the channel, but getFull can get the .total. @@ -130,22 +127,13 @@ class _ParticipantsIter(requestiter.RequestIter): raise StopAsyncIteration self.seen = set() - if aggressive and not filter: - self.requests.extend(_tl.fn.channels.GetParticipants( - channel=entity, - filter=_tl.ChannelParticipantsSearch(x), - offset=0, - limit=_MAX_PARTICIPANTS_CHUNK_SIZE, - hash=0 - ) for x in (search or string.ascii_lowercase)) - else: - self.requests.append(_tl.fn.channels.GetParticipants( - channel=entity, - filter=filter or _tl.ChannelParticipantsSearch(search), - offset=0, - limit=_MAX_PARTICIPANTS_CHUNK_SIZE, - hash=0 - )) + self.request = _tl.fn.channels.GetParticipants( + channel=entity, + filter=filter or _tl.ChannelParticipantsSearch(search), + offset=0, + limit=_MAX_PARTICIPANTS_CHUNK_SIZE, + hash=0 + ) elif ty == helpers._EntityType.CHAT: full = await self.client( @@ -184,24 +172,21 @@ class _ParticipantsIter(requestiter.RequestIter): return True async def _load_next_chunk(self): - if not self.requests: - return True - # Only care about the limit for the first request - # (small amount of people, won't be aggressive). + # (small amount of people). # # Most people won't care about getting exactly 12,345 # members so it doesn't really matter not to be 100% # precise with being out of the offset/limit here. - self.requests[0].limit = min( - self.limit - self.requests[0].offset, _MAX_PARTICIPANTS_CHUNK_SIZE) + self.request.limit = min( + self.limit - self.request.offset, _MAX_PARTICIPANTS_CHUNK_SIZE) - if self.requests[0].offset > self.limit: + if self.request.offset > self.limit: return True if self.total is None: - f = self.requests[0].filter - if len(self.requests) > 1 or ( + f = self.request.filter + if ( not isinstance(f, _tl.ChannelParticipantsRecent) and (not isinstance(f, _tl.ChannelParticipantsSearch) or f.q) ): @@ -209,42 +194,36 @@ class _ParticipantsIter(requestiter.RequestIter): # if there's a filter which would reduce the real total number. # getParticipants is cheaper than getFull. self.total = (await self.client(_tl.fn.channels.GetParticipants( - channel=self.requests[0].channel, + channel=self.request.channel, filter=_tl.ChannelParticipantsRecent(), offset=0, limit=1, hash=0 ))).count - results = await self.client(self.requests) - for i in reversed(range(len(self.requests))): - participants = results[i] - if self.total is None: - # Will only get here if there was one request with a filter that matched all users. - self.total = participants.count - if not participants.users: - self.requests.pop(i) - continue + participants = await self.client(self.request) + if self.total is None: + # Will only get here if there was one request with a filter that matched all users. + self.total = participants.count - self.requests[i].offset += len(participants.participants) - users = {user.id: user for user in participants.users} - for participant in participants.participants: - - if isinstance(participant, _tl.ChannelParticipantBanned): - if not isinstance(participant.peer, _tl.PeerUser): - # May have the entire channel banned. See #3105. - continue - user_id = participant.peer.user_id - else: - user_id = participant.user_id - - user = users[user_id] - if not self.filter_entity(user) or user.id in self.seen: + self.request.offset += len(participants.participants) + users = {user.id: user for user in participants.users} + for participant in participants.participants: + if isinstance(participant, _tl.ChannelParticipantBanned): + if not isinstance(participant.peer, _tl.PeerUser): + # May have the entire channel banned. See #3105. continue - self.seen.add(user_id) - user = users[user_id] - user.participant = participant - self.buffer.append(user) + user_id = participant.peer.user_id + else: + user_id = participant.user_id + + user = users[user_id] + if not self.filter_entity(user) or user.id in self.seen: + continue + self.seen.add(user_id) + user = users[user_id] + user.participant = participant + self.buffer.append(user) class _AdminLogIter(requestiter.RequestIter): @@ -407,15 +386,13 @@ def get_participants( limit: float = None, *, search: str = '', - filter: '_tl.TypeChannelParticipantsFilter' = None, - aggressive: bool = False) -> _ParticipantsIter: + filter: '_tl.TypeChannelParticipantsFilter' = None) -> _ParticipantsIter: return _ParticipantsIter( self, limit, entity=entity, filter=filter, - search=search, - aggressive=aggressive + search=search ) diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 1501fee3..1a9e7bef 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -722,8 +722,7 @@ class TelegramClient: limit: float = (), *, search: str = '', - filter: '_tl.TypeChannelParticipantsFilter' = None, - aggressive: bool = False) -> chats._ParticipantsIter: + filter: '_tl.TypeChannelParticipantsFilter' = None) -> chats._ParticipantsIter: """ Iterator over the participants belonging to the specified chat. @@ -739,9 +738,6 @@ class TelegramClient: search (`str`, optional): Look for participants with this string in name/username. - If ``aggressive is True``, the symbols from this string will - be used. - filter (:tl:`ChannelParticipantsFilter`, optional): The filter to be used, if you want e.g. only admins Note that you might not have permissions for some filter. @@ -753,16 +749,6 @@ class TelegramClient: *restricted* users. If you want *banned* users you should use :tl:`ChannelParticipantsKicked` instead. - aggressive (`bool`, optional): - Aggressively looks for all participants in the chat. - - This is useful for channels since 20 July 2018, - Telegram added a server-side limit where only the - first 200 members can be retrieved. With this flag - set, more than 200 will be often be retrieved. - - This has no effect if a ``filter`` is given. - Yields The :tl:`User` objects returned by :tl:`GetParticipants` with an additional ``.participant`` attribute which is the From 6e9ad9e31c5a7e5cf2ebccf18d2f15aae442b0ee Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 17 Sep 2021 20:16:01 +0200 Subject: [PATCH 057/256] Return correct total participant count when a filter is desired --- readthedocs/misc/v2-migration-guide.rst | 10 ++++++++++ telethon/_client/chats.py | 21 +-------------------- 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 628b7a95..5e9d6a5d 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -225,6 +225,16 @@ The "aggressive" hack in ``get_participants`` (and ``iter_participants``) is now It was not reliable, and was a cause of flood wait errors. +The total value when getting participants has changed +----------------------------------------------------- + +Before, it used to always be the total amount of people inside the chat. Now the filter is also +considered. If you were running ``client.get_participants`` with a ``filter`` other than the +default and accessing the ``list.total``, you will now get a different result. You will need to +perform a separate request with no filter to fetch the total without filter (this is what the +library used to do). + + The TelegramClient is no longer made out of mixins -------------------------------------------------- diff --git a/telethon/_client/chats.py b/telethon/_client/chats.py index 3fcce1b4..1aa0724d 100644 --- a/telethon/_client/chats.py +++ b/telethon/_client/chats.py @@ -184,27 +184,8 @@ class _ParticipantsIter(requestiter.RequestIter): if self.request.offset > self.limit: return True - if self.total is None: - f = self.request.filter - if ( - not isinstance(f, _tl.ChannelParticipantsRecent) - and (not isinstance(f, _tl.ChannelParticipantsSearch) or f.q) - ): - # Only do an additional getParticipants here to get the total - # if there's a filter which would reduce the real total number. - # getParticipants is cheaper than getFull. - self.total = (await self.client(_tl.fn.channels.GetParticipants( - channel=self.request.channel, - filter=_tl.ChannelParticipantsRecent(), - offset=0, - limit=1, - hash=0 - ))).count - participants = await self.client(self.request) - if self.total is None: - # Will only get here if there was one request with a filter that matched all users. - self.total = participants.count + self.total = participants.count self.request.offset += len(participants.participants) users = {user.id: user for user in participants.users} From 40ff7c6bdf29f178da1df341bd4dc2f3685e2906 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 17 Sep 2021 20:19:23 +0200 Subject: [PATCH 058/256] Document default behaviour of limit --- telethon/_client/telegramclient.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 1a9e7bef..30c2811a 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -735,6 +735,10 @@ class TelegramClient: limit (`int`): Limits amount of participants fetched. + By default, there is no limit set when using the result as + an iterator. When using ``await``, the default limit is 1, + so the method returns a single user. + search (`str`, optional): Look for participants with this string in name/username. @@ -822,6 +826,10 @@ class TelegramClient: The limit may also be `None`, which would eventually return the whole history. + By default, there is no limit set when using the result as + an iterator. When using ``await``, the default limit is 1, + so the method returns the last event. + max_id (`int`): All the events with a higher (newer) ID or equal to this will be excluded. @@ -930,6 +938,10 @@ class TelegramClient: The limit may also be `None`, which would eventually all the photos that are still available. + By default, there is no limit set when using the result as + an iterator. When using ``await``, the default limit is 1, + so the method returns the last profile photo. + offset (`int`): How many photos should be skipped before returning the first one. @@ -1407,6 +1419,10 @@ class TelegramClient: will tell the library to slow down through a ``FloodWaitError``. + By default, there is no limit set when using the result as + an iterator. When using ``await``, the default limit is 1, + so the method returns the most-recent dialog. + offset_date (`datetime`, optional): The offset date to be used. @@ -2001,6 +2017,10 @@ class TelegramClient: The limit may also be `None`, which would eventually return the whole history. + By default, there is no limit set when using the result as + an iterator. When using ``await``, the default limit is 1, + so the method returns the last message. + offset_date (`datetime`): Offset date (messages *previous* to this date will be retrieved). Exclusive. From d81ebe92f7ae146d0d4ac1e183cc0243634a880f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 17 Sep 2021 20:25:53 +0200 Subject: [PATCH 059/256] Remove Wall of Shame People make mistakes. Get over it. No need to be a child about it. --- readthedocs/index.rst | 1 - readthedocs/misc/wall-of-shame.rst | 65 ------------------------------ 2 files changed, 66 deletions(-) delete mode 100644 readthedocs/misc/wall-of-shame.rst diff --git a/readthedocs/index.rst b/readthedocs/index.rst index 1794ce72..91d08e0f 100644 --- a/readthedocs/index.rst +++ b/readthedocs/index.rst @@ -108,7 +108,6 @@ You can also use the menu on the left to quickly skip over sections. misc/changelog misc/v2-migration-guide.rst - misc/wall-of-shame.rst misc/compatibility-and-convenience .. toctree:: diff --git a/readthedocs/misc/wall-of-shame.rst b/readthedocs/misc/wall-of-shame.rst deleted file mode 100644 index 87be0464..00000000 --- a/readthedocs/misc/wall-of-shame.rst +++ /dev/null @@ -1,65 +0,0 @@ -============= -Wall of Shame -============= - - -This project has an -`issues `__ section for -you to file **issues** whenever you encounter any when working with the -library. Said section is **not** for issues on *your* program but rather -issues with Telethon itself. - -If you have not made the effort to 1. read through the docs and 2. -`look for the method you need `__, -you will end up on the `Wall of -Shame `__, -i.e. all issues labeled -`"RTFM" `__: - - **rtfm** - Literally "Read The F--king Manual"; a term showing the - frustration of being bothered with questions so trivial that the asker - could have quickly figured out the answer on their own with minimal - effort, usually by reading readily-available documents. People who - say"RTFM!" might be considered rude, but the true rude ones are the - annoying people who take absolutely no self-responibility and expect to - have all the answers handed to them personally. - - *"Damn, that's the twelveth time that somebody posted this question - to the messageboard today! RTFM, already!"* - - *by Bill M. July 27, 2004* - -If you have indeed read the docs, and have tried looking for the method, -and yet you didn't find what you need, **that's fine**. Telegram's API -can have some obscure names at times, and for this reason, there is a -`"question" -label `__ -with questions that are okay to ask. Just state what you've tried so -that we know you've made an effort, or you'll go to the Wall of Shame. - -Of course, if the issue you're going to open is not even a question but -a real issue with the library (thankfully, most of the issues have been -that!), you won't end up here. Don't worry. - -Current winner --------------- - -The current winner is `issue -213 `__: - -**Issue:** - -.. figure:: https://user-images.githubusercontent.com/6297805/29822978-9a9a6ef0-8ccd-11e7-9ec5-934ea0f57681.jpg - -:alt: Winner issue - -Winner issue - -**Answer:** - -.. figure:: https://user-images.githubusercontent.com/6297805/29822983-9d523402-8ccd-11e7-9fb1-5783740ee366.jpg - -:alt: Winner issue answer - -Winner issue answer From b3c23e343a6f04a01327bd738f2e21f903d75e6d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 17 Sep 2021 20:36:40 +0200 Subject: [PATCH 060/256] Return deleted count from delete_messages --- readthedocs/misc/v2-migration-guide.rst | 10 +++++++++- telethon/_client/messages.py | 10 ++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 5e9d6a5d..e1621fb4 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -13,7 +13,7 @@ good chance you were not relying on this to begin with". **Please read this document in full before upgrading your code to Telethon 2.0.** -Pyhton 3.5 is no longer supported +Python 3.5 is no longer supported --------------------------------- The library will no longer attempt to support Python 3.5. The minimum version is now Python 3.6. @@ -218,6 +218,14 @@ your handlers much more easily. // TODO provide standalone alternative for this? +Deleting messages now returns a more useful value +------------------------------------------------- + +It used to return a list of :tl:`messages.affectedMessages` which I expect very little people were +actually using. Now it returns an ``int`` value indicating the number of messages that did exist +and were deleted. + + The aggressive parameter hack has been removed ---------------------------------------------- diff --git a/telethon/_client/messages.py b/telethon/_client/messages.py index c29610e4..5c073e60 100644 --- a/telethon/_client/messages.py +++ b/telethon/_client/messages.py @@ -644,11 +644,13 @@ async def delete_messages( ty = helpers._EntityType.USER if ty == helpers._EntityType.CHANNEL: - return await self([_tl.fn.channels.DeleteMessages( - entity, list(c)) for c in utils.chunks(message_ids)]) + res = await self([_tl.fn.channels.DeleteMessages( + entity, list(c)) for c in utils.chunks(message_ids)]) else: - return await self([_tl.fn.messages.DeleteMessages( - list(c), revoke) for c in utils.chunks(message_ids)]) + res = await self([_tl.fn.messages.DeleteMessages( + list(c), revoke) for c in utils.chunks(message_ids)]) + + return sum(r.pts_count for r in res) async def send_read_acknowledge( self: 'TelegramClient', From 3bc46e80728272e18a43149647d48ce272a43ea7 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 17 Sep 2021 20:55:27 +0200 Subject: [PATCH 061/256] Remove broken CdnDecrypter --- readthedocs/misc/v2-migration-guide.rst | 6 ++ telethon/_client/telegrambaseclient.py | 41 ++-------- telethon/_client/telegramclient.py | 1 - telethon/_crypto/__init__.py | 1 - telethon/_crypto/cdndecrypter.py | 104 ------------------------ 5 files changed, 11 insertions(+), 142 deletions(-) delete mode 100644 telethon/_crypto/cdndecrypter.py diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index e1621fb4..7e7a2302 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -249,3 +249,9 @@ The TelegramClient is no longer made out of mixins If you were relying on any of the individual mixins that made up the client, such as ``UserMethods`` inside the ``telethon.client`` subpackage, those are now gone. There is a single ``TelegramClient`` class now, containing everything you need. + + +CdnDecrypter has been removed +----------------------------- + +It was not really working and was more intended to be an implementation detail than anything else. diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index d8d333f0..35507edd 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -428,31 +428,26 @@ def _auth_key_callback(self: 'TelegramClient', auth_key): self.session.save() -async def _get_dc(self: 'TelegramClient', dc_id, cdn=False): +async def _get_dc(self: 'TelegramClient', dc_id): """Gets the Data Center (DC) associated to 'dc_id'""" cls = self.__class__ if not cls._config: cls._config = await self(_tl.fn.help.GetConfig()) - if cdn and not self._cdn_config: - cls._cdn_config = await self(_tl.fn.help.GetCdnConfig()) - for pk in cls._cdn_config.public_keys: - rsa.add_key(pk.public_key) - try: return next( dc for dc in cls._config.dc_options if dc.id == dc_id - and bool(dc.ipv6) == self._use_ipv6 and bool(dc.cdn) == cdn + and bool(dc.ipv6) == self._use_ipv6 and not dc.cdn ) except StopIteration: self._log[__name__].warning( - 'Failed to get DC %s (cdn = %s) with use_ipv6 = %s; retrying ignoring IPv6 check', - dc_id, cdn, self._use_ipv6 + 'Failed to get DC %swith use_ipv6 = %s; retrying ignoring IPv6 check', + dc_id, self._use_ipv6 ) return next( dc for dc in cls._config.dc_options - if dc.id == dc_id and bool(dc.cdn) == cdn + if dc.id == dc_id and not dc.cdn ) async def _create_exported_sender(self: 'TelegramClient', dc_id): @@ -538,29 +533,3 @@ async def _clean_exported_senders(self: 'TelegramClient'): # Disconnect should never raise await sender.disconnect() state.mark_disconnected() - -async def _get_cdn_client(self: 'TelegramClient', cdn_redirect): - """Similar to ._borrow_exported_client, but for CDNs""" - # TODO Implement - raise NotImplementedError - session = self._exported_sessions.get(cdn_redirect.dc_id) - if not session: - dc = await _get_dc(self, cdn_redirect.dc_id, cdn=True) - session = self.session.clone() - await session.set_dc(dc.id, dc.ip_address, dc.port) - self._exported_sessions[cdn_redirect.dc_id] = session - - self._log[__name__].info('Creating new CDN client') - client = TelegramBaseClient( - session, self.api_id, self.api_hash, - proxy=self._sender.connection.conn.proxy, - timeout=self._sender.connection.get_timeout() - ) - - # This will make use of the new RSA keys for this specific CDN. - # - # We won't be calling GetConfig because it's only called - # when needed by ._get_dc, and also it's static so it's likely - # set already. Avoid invoking non-CDN methods by not syncing updates. - client.connect(_sync_updates=False) - return client diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 30c2811a..cb644dd2 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -2761,7 +2761,6 @@ class TelegramClient: # Cached server configuration (with .dc_options), can be "global" _config = None - _cdn_config = None def __init__( self: 'TelegramClient', diff --git a/telethon/_crypto/__init__.py b/telethon/_crypto/__init__.py index 69be1da8..f10c9ad7 100644 --- a/telethon/_crypto/__init__.py +++ b/telethon/_crypto/__init__.py @@ -7,4 +7,3 @@ from .aes import AES from .aesctr import AESModeCTR from .authkey import AuthKey from .factorization import Factorization -from .cdndecrypter import CdnDecrypter diff --git a/telethon/_crypto/cdndecrypter.py b/telethon/_crypto/cdndecrypter.py deleted file mode 100644 index 73a568d1..00000000 --- a/telethon/_crypto/cdndecrypter.py +++ /dev/null @@ -1,104 +0,0 @@ -""" -This module holds the CdnDecrypter utility class. -""" -from hashlib import sha256 - -from .. import _tl -from .._crypto import AESModeCTR -from ..errors import CdnFileTamperedError - - -class CdnDecrypter: - """ - Used when downloading a file results in a 'FileCdnRedirect' to - both prepare the redirect, decrypt the file as it downloads, and - ensure the file hasn't been tampered. https://core.telegram.org/cdn - """ - def __init__(self, cdn_client, file_token, cdn_aes, cdn_file_hashes): - """ - Initializes the CDN decrypter. - - :param cdn_client: a client connected to a CDN. - :param file_token: the token of the file to be used. - :param cdn_aes: the AES CTR used to decrypt the file. - :param cdn_file_hashes: the hashes the decrypted file must match. - """ - self.client = cdn_client - self.file_token = file_token - self.cdn_aes = cdn_aes - self.cdn_file_hashes = cdn_file_hashes - - @staticmethod - async def prepare_decrypter(client, cdn_client, cdn_redirect): - """ - Prepares a new CDN decrypter. - - :param client: a TelegramClient connected to the main servers. - :param cdn_client: a new client connected to the CDN. - :param cdn_redirect: the redirect file object that caused this call. - :return: (CdnDecrypter, first chunk file data) - """ - cdn_aes = AESModeCTR( - key=cdn_redirect.encryption_key, - # 12 first bytes of the IV..4 bytes of the offset (0, big endian) - iv=cdn_redirect.encryption_iv[:12] + bytes(4) - ) - - # We assume that cdn_redirect.cdn_file_hashes are ordered by offset, - # and that there will be enough of these to retrieve the whole file. - decrypter = CdnDecrypter( - cdn_client, cdn_redirect.file_token, - cdn_aes, cdn_redirect.cdn_file_hashes - ) - - cdn_file = await cdn_client(_tl.fn.upload.GetCdnFile( - file_token=cdn_redirect.file_token, - offset=cdn_redirect.cdn_file_hashes[0].offset, - limit=cdn_redirect.cdn_file_hashes[0].limit - )) - if isinstance(cdn_file, _tl.upload.CdnFileReuploadNeeded): - # We need to use the original client here - await client(_tl.fn.upload.ReuploadCdnFile( - file_token=cdn_redirect.file_token, - request_token=cdn_file.request_token - )) - - # We want to always return a valid upload.CdnFile - cdn_file = decrypter.get_file() - else: - cdn_file.bytes = decrypter.cdn_aes.encrypt(cdn_file.bytes) - cdn_hash = decrypter.cdn_file_hashes.pop(0) - decrypter.check(cdn_file.bytes, cdn_hash) - - return decrypter, cdn_file - - def get_file(self): - """ - Calls GetCdnFile and decrypts its bytes. - Also ensures that the file hasn't been tampered. - - :return: the CdnFile result. - """ - if self.cdn_file_hashes: - cdn_hash = self.cdn_file_hashes.pop(0) - cdn_file = self.client(_tl.fn.upload.GetCdnFile( - self.file_token, cdn_hash.offset, cdn_hash.limit - )) - cdn_file.bytes = self.cdn_aes.encrypt(cdn_file.bytes) - self.check(cdn_file.bytes, cdn_hash) - else: - cdn_file = _tl.upload.CdnFile(bytes(0)) - - return cdn_file - - @staticmethod - def check(data, cdn_hash): - """ - Checks the integrity of the given data. - Raises CdnFileTamperedError if the integrity check fails. - - :param data: the data to be hashed. - :param cdn_hash: the expected hash. - """ - if sha256(data).digest() != cdn_hash.hash: - raise CdnFileTamperedError() From dc29a95cef8deca2f18cb1cc95f8ba879a6512b9 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 17 Sep 2021 21:03:47 +0200 Subject: [PATCH 062/256] Change list of buttons to show up as rows and not cols --- readthedocs/misc/v2-migration-guide.rst | 27 +++++++++++++++++++++++++ telethon/_client/buttons.py | 6 +++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 7e7a2302..4276dec8 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -204,6 +204,33 @@ either ``.md_text`` or ``.html_text`` as needed. This is because both ``.text`` may disappear in future versions, and their behaviour is not immediately obvious. +Using a flat list to define buttons will now create rows and not columns +------------------------------------------------------------------------ + +When sending a message with buttons under a bot account, passing a flat list such as the following: + +.. code-block:: python + + bot.send_message(chat, message, buttons=[ + Button.inline('top'), + Button.inline('middle'), + Button.inline('bottom'), + ]) + +Will now send a message with 3 rows of buttons, instead of a message with 3 columns (old behaviour). +If you still want the old behaviour, wrap the list inside another list: + +.. code-block:: python + + bot.send_message(chat, message, buttons=[[ + # + + Button.inline('top'), + Button.inline('middle'), + Button.inline('bottom'), + ]]) + #+ + + The Conversation API has been removed ------------------------------------- diff --git a/telethon/_client/buttons.py b/telethon/_client/buttons.py index fbd0f51c..599ebc96 100644 --- a/telethon/_client/buttons.py +++ b/telethon/_client/buttons.py @@ -8,7 +8,7 @@ from ..types import _custom def build_reply_markup( buttons: 'typing.Optional[hints.MarkupLike]', inline_only: bool = False) -> 'typing.Optional[_tl.TypeReplyMarkup]': - if buttons is None: + if not buttons: return None try: @@ -18,9 +18,9 @@ def build_reply_markup( pass if not utils.is_list_like(buttons): - buttons = [[buttons]] - elif not buttons or not utils.is_list_like(buttons[0]): buttons = [buttons] + if not utils.is_list_like(buttons[0]): + buttons = [[b] for b in buttons] is_inline = False is_normal = False From 783c1771abe6a8e7ab552c38ef376ccd3f7116fa Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 17 Sep 2021 21:05:09 +0200 Subject: [PATCH 063/256] Fix remaining uses of old types namespace --- telethon/types/_custom/adminlogevent.py | 144 +++++++++--------- telethon/types/_custom/button.py | 38 ++--- telethon/types/_custom/inlineresult.py | 10 +- .../types/_custom/participantpermissions.py | 18 +-- telethon/types/_custom/qrlogin.py | 6 +- 5 files changed, 108 insertions(+), 108 deletions(-) diff --git a/telethon/types/_custom/adminlogevent.py b/telethon/types/_custom/adminlogevent.py index 6d8af269..8c63a954 100644 --- a/telethon/types/_custom/adminlogevent.py +++ b/telethon/types/_custom/adminlogevent.py @@ -64,43 +64,43 @@ class AdminLogEvent: """ ori = self.original.action if isinstance(ori, ( - types.ChannelAdminLogEventActionChangeAbout, - types.ChannelAdminLogEventActionChangeTitle, - types.ChannelAdminLogEventActionChangeUsername, - types.ChannelAdminLogEventActionChangeLocation, - types.ChannelAdminLogEventActionChangeHistoryTTL, + _tl.ChannelAdminLogEventActionChangeAbout, + _tl.ChannelAdminLogEventActionChangeTitle, + _tl.ChannelAdminLogEventActionChangeUsername, + _tl.ChannelAdminLogEventActionChangeLocation, + _tl.ChannelAdminLogEventActionChangeHistoryTTL, )): return ori.prev_value - elif isinstance(ori, types.ChannelAdminLogEventActionChangePhoto): + elif isinstance(ori, _tl.ChannelAdminLogEventActionChangePhoto): return ori.prev_photo - elif isinstance(ori, types.ChannelAdminLogEventActionChangeStickerSet): + elif isinstance(ori, _tl.ChannelAdminLogEventActionChangeStickerSet): return ori.prev_stickerset - elif isinstance(ori, types.ChannelAdminLogEventActionEditMessage): + elif isinstance(ori, _tl.ChannelAdminLogEventActionEditMessage): return ori.prev_message elif isinstance(ori, ( - types.ChannelAdminLogEventActionParticipantToggleAdmin, - types.ChannelAdminLogEventActionParticipantToggleBan + _tl.ChannelAdminLogEventActionParticipantToggleAdmin, + _tl.ChannelAdminLogEventActionParticipantToggleBan )): return ori.prev_participant elif isinstance(ori, ( - types.ChannelAdminLogEventActionToggleInvites, - types.ChannelAdminLogEventActionTogglePreHistoryHidden, - types.ChannelAdminLogEventActionToggleSignatures + _tl.ChannelAdminLogEventActionToggleInvites, + _tl.ChannelAdminLogEventActionTogglePreHistoryHidden, + _tl.ChannelAdminLogEventActionToggleSignatures )): return not ori.new_value - elif isinstance(ori, types.ChannelAdminLogEventActionDeleteMessage): + elif isinstance(ori, _tl.ChannelAdminLogEventActionDeleteMessage): return ori.message - elif isinstance(ori, types.ChannelAdminLogEventActionDefaultBannedRights): + elif isinstance(ori, _tl.ChannelAdminLogEventActionDefaultBannedRights): return ori.prev_banned_rights - elif isinstance(ori, types.ChannelAdminLogEventActionDiscardGroupCall): + elif isinstance(ori, _tl.ChannelAdminLogEventActionDiscardGroupCall): return ori.call elif isinstance(ori, ( - types.ChannelAdminLogEventActionExportedInviteDelete, - types.ChannelAdminLogEventActionExportedInviteRevoke, - types.ChannelAdminLogEventActionParticipantJoinByInvite, + _tl.ChannelAdminLogEventActionExportedInviteDelete, + _tl.ChannelAdminLogEventActionExportedInviteRevoke, + _tl.ChannelAdminLogEventActionParticipantJoinByInvite, )): return ori.invite - elif isinstance(ori, types.ChannelAdminLogEventActionExportedInviteEdit): + elif isinstance(ori, _tl.ChannelAdminLogEventActionExportedInviteEdit): return ori.prev_invite @property @@ -110,46 +110,46 @@ class AdminLogEvent: """ ori = self.original.action if isinstance(ori, ( - types.ChannelAdminLogEventActionChangeAbout, - types.ChannelAdminLogEventActionChangeTitle, - types.ChannelAdminLogEventActionChangeUsername, - types.ChannelAdminLogEventActionToggleInvites, - types.ChannelAdminLogEventActionTogglePreHistoryHidden, - types.ChannelAdminLogEventActionToggleSignatures, - types.ChannelAdminLogEventActionChangeLocation, - types.ChannelAdminLogEventActionChangeHistoryTTL, + _tl.ChannelAdminLogEventActionChangeAbout, + _tl.ChannelAdminLogEventActionChangeTitle, + _tl.ChannelAdminLogEventActionChangeUsername, + _tl.ChannelAdminLogEventActionToggleInvites, + _tl.ChannelAdminLogEventActionTogglePreHistoryHidden, + _tl.ChannelAdminLogEventActionToggleSignatures, + _tl.ChannelAdminLogEventActionChangeLocation, + _tl.ChannelAdminLogEventActionChangeHistoryTTL, )): return ori.new_value - elif isinstance(ori, types.ChannelAdminLogEventActionChangePhoto): + elif isinstance(ori, _tl.ChannelAdminLogEventActionChangePhoto): return ori.new_photo - elif isinstance(ori, types.ChannelAdminLogEventActionChangeStickerSet): + elif isinstance(ori, _tl.ChannelAdminLogEventActionChangeStickerSet): return ori.new_stickerset - elif isinstance(ori, types.ChannelAdminLogEventActionEditMessage): + elif isinstance(ori, _tl.ChannelAdminLogEventActionEditMessage): return ori.new_message elif isinstance(ori, ( - types.ChannelAdminLogEventActionParticipantToggleAdmin, - types.ChannelAdminLogEventActionParticipantToggleBan + _tl.ChannelAdminLogEventActionParticipantToggleAdmin, + _tl.ChannelAdminLogEventActionParticipantToggleBan )): return ori.new_participant elif isinstance(ori, ( - types.ChannelAdminLogEventActionParticipantInvite, - types.ChannelAdminLogEventActionParticipantVolume, + _tl.ChannelAdminLogEventActionParticipantInvite, + _tl.ChannelAdminLogEventActionParticipantVolume, )): return ori.participant - elif isinstance(ori, types.ChannelAdminLogEventActionDefaultBannedRights): + elif isinstance(ori, _tl.ChannelAdminLogEventActionDefaultBannedRights): return ori.new_banned_rights - elif isinstance(ori, types.ChannelAdminLogEventActionStopPoll): + elif isinstance(ori, _tl.ChannelAdminLogEventActionStopPoll): return ori.message - elif isinstance(ori, types.ChannelAdminLogEventActionStartGroupCall): + elif isinstance(ori, _tl.ChannelAdminLogEventActionStartGroupCall): return ori.call elif isinstance(ori, ( - types.ChannelAdminLogEventActionParticipantMute, - types.ChannelAdminLogEventActionParticipantUnmute, + _tl.ChannelAdminLogEventActionParticipantMute, + _tl.ChannelAdminLogEventActionParticipantUnmute, )): return ori.participant - elif isinstance(ori, types.ChannelAdminLogEventActionToggleGroupCallSetting): + elif isinstance(ori, _tl.ChannelAdminLogEventActionToggleGroupCallSetting): return ori.join_muted - elif isinstance(ori, types.ChannelAdminLogEventActionExportedInviteEdit): + elif isinstance(ori, _tl.ChannelAdminLogEventActionExportedInviteEdit): return ori.new_invite @property @@ -160,7 +160,7 @@ class AdminLogEvent: If `True`, `old` and `new` will be present as `str`. """ return isinstance(self.original.action, - types.ChannelAdminLogEventActionChangeAbout) + _tl.ChannelAdminLogEventActionChangeAbout) @property def changed_title(self): @@ -170,7 +170,7 @@ class AdminLogEvent: If `True`, `old` and `new` will be present as `str`. """ return isinstance(self.original.action, - types.ChannelAdminLogEventActionChangeTitle) + _tl.ChannelAdminLogEventActionChangeTitle) @property def changed_username(self): @@ -180,7 +180,7 @@ class AdminLogEvent: If `True`, `old` and `new` will be present as `str`. """ return isinstance(self.original.action, - types.ChannelAdminLogEventActionChangeUsername) + _tl.ChannelAdminLogEventActionChangeUsername) @property def changed_photo(self): @@ -190,7 +190,7 @@ class AdminLogEvent: If `True`, `old` and `new` will be present as :tl:`Photo`. """ return isinstance(self.original.action, - types.ChannelAdminLogEventActionChangePhoto) + _tl.ChannelAdminLogEventActionChangePhoto) @property def changed_sticker_set(self): @@ -200,7 +200,7 @@ class AdminLogEvent: If `True`, `old` and `new` will be present as :tl:`InputStickerSet`. """ return isinstance(self.original.action, - types.ChannelAdminLogEventActionChangeStickerSet) + _tl.ChannelAdminLogEventActionChangeStickerSet) @property def changed_message(self): @@ -211,7 +211,7 @@ class AdminLogEvent: `Message `. """ return isinstance(self.original.action, - types.ChannelAdminLogEventActionEditMessage) + _tl.ChannelAdminLogEventActionEditMessage) @property def deleted_message(self): @@ -222,7 +222,7 @@ class AdminLogEvent: `Message `. """ return isinstance(self.original.action, - types.ChannelAdminLogEventActionDeleteMessage) + _tl.ChannelAdminLogEventActionDeleteMessage) @property def changed_admin(self): @@ -235,7 +235,7 @@ class AdminLogEvent: """ return isinstance( self.original.action, - types.ChannelAdminLogEventActionParticipantToggleAdmin) + _tl.ChannelAdminLogEventActionParticipantToggleAdmin) @property def changed_restrictions(self): @@ -247,7 +247,7 @@ class AdminLogEvent: """ return isinstance( self.original.action, - types.ChannelAdminLogEventActionParticipantToggleBan) + _tl.ChannelAdminLogEventActionParticipantToggleBan) @property def changed_invites(self): @@ -257,7 +257,7 @@ class AdminLogEvent: If `True`, `old` and `new` will be present as `bool`. """ return isinstance(self.original.action, - types.ChannelAdminLogEventActionToggleInvites) + _tl.ChannelAdminLogEventActionToggleInvites) @property def changed_location(self): @@ -267,7 +267,7 @@ class AdminLogEvent: If `True`, `old` and `new` will be present as :tl:`ChannelLocation`. """ return isinstance(self.original.action, - types.ChannelAdminLogEventActionChangeLocation) + _tl.ChannelAdminLogEventActionChangeLocation) @property def joined(self): @@ -276,7 +276,7 @@ class AdminLogEvent: public username or not. """ return isinstance(self.original.action, - types.ChannelAdminLogEventActionParticipantJoin) + _tl.ChannelAdminLogEventActionParticipantJoin) @property def joined_invite(self): @@ -288,7 +288,7 @@ class AdminLogEvent: :tl:`ChannelParticipant`. """ return isinstance(self.original.action, - types.ChannelAdminLogEventActionParticipantInvite) + _tl.ChannelAdminLogEventActionParticipantInvite) @property def left(self): @@ -296,7 +296,7 @@ class AdminLogEvent: Whether `user` left the channel or not. """ return isinstance(self.original.action, - types.ChannelAdminLogEventActionParticipantLeave) + _tl.ChannelAdminLogEventActionParticipantLeave) @property def changed_hide_history(self): @@ -307,7 +307,7 @@ class AdminLogEvent: If `True`, `old` and `new` will be present as `bool`. """ return isinstance(self.original.action, - types.ChannelAdminLogEventActionTogglePreHistoryHidden) + _tl.ChannelAdminLogEventActionTogglePreHistoryHidden) @property def changed_signatures(self): @@ -318,7 +318,7 @@ class AdminLogEvent: If `True`, `old` and `new` will be present as `bool`. """ return isinstance(self.original.action, - types.ChannelAdminLogEventActionToggleSignatures) + _tl.ChannelAdminLogEventActionToggleSignatures) @property def changed_pin(self): @@ -329,7 +329,7 @@ class AdminLogEvent: `Message `. """ return isinstance(self.original.action, - types.ChannelAdminLogEventActionUpdatePinned) + _tl.ChannelAdminLogEventActionUpdatePinned) @property def changed_default_banned_rights(self): @@ -340,7 +340,7 @@ class AdminLogEvent: be present as :tl:`ChatBannedRights`. """ return isinstance(self.original.action, - types.ChannelAdminLogEventActionDefaultBannedRights) + _tl.ChannelAdminLogEventActionDefaultBannedRights) @property def stopped_poll(self): @@ -351,7 +351,7 @@ class AdminLogEvent: `Message `. """ return isinstance(self.original.action, - types.ChannelAdminLogEventActionStopPoll) + _tl.ChannelAdminLogEventActionStopPoll) @property def started_group_call(self): @@ -361,7 +361,7 @@ class AdminLogEvent: If `True`, `new` will be present as :tl:`InputGroupCall`. """ return isinstance(self.original.action, - types.ChannelAdminLogEventActionStartGroupCall) + _tl.ChannelAdminLogEventActionStartGroupCall) @property def discarded_group_call(self): @@ -371,7 +371,7 @@ class AdminLogEvent: If `True`, `old` will be present as :tl:`InputGroupCall`. """ return isinstance(self.original.action, - types.ChannelAdminLogEventActionDiscardGroupCall) + _tl.ChannelAdminLogEventActionDiscardGroupCall) @property def user_muted(self): @@ -381,7 +381,7 @@ class AdminLogEvent: If `True`, `new` will be present as :tl:`GroupCallParticipant`. """ return isinstance(self.original.action, - types.ChannelAdminLogEventActionParticipantMute) + _tl.ChannelAdminLogEventActionParticipantMute) @property def user_unmutted(self): @@ -391,7 +391,7 @@ class AdminLogEvent: If `True`, `new` will be present as :tl:`GroupCallParticipant`. """ return isinstance(self.original.action, - types.ChannelAdminLogEventActionParticipantUnmute) + _tl.ChannelAdminLogEventActionParticipantUnmute) @property def changed_call_settings(self): @@ -401,7 +401,7 @@ class AdminLogEvent: If `True`, `new` will be `True` if new users are muted on join. """ return isinstance(self.original.action, - types.ChannelAdminLogEventActionToggleGroupCallSetting) + _tl.ChannelAdminLogEventActionToggleGroupCallSetting) @property def changed_history_ttl(self): @@ -414,7 +414,7 @@ class AdminLogEvent: If `True`, `old` will be the old TTL, and `new` the new TTL, in seconds. """ return isinstance(self.original.action, - types.ChannelAdminLogEventActionChangeHistoryTTL) + _tl.ChannelAdminLogEventActionChangeHistoryTTL) @property def deleted_exported_invite(self): @@ -424,7 +424,7 @@ class AdminLogEvent: If `True`, `old` will be the deleted :tl:`ExportedChatInvite`. """ return isinstance(self.original.action, - types.ChannelAdminLogEventActionExportedInviteDelete) + _tl.ChannelAdminLogEventActionExportedInviteDelete) @property def edited_exported_invite(self): @@ -435,7 +435,7 @@ class AdminLogEvent: :tl:`ExportedChatInvite`, respectively. """ return isinstance(self.original.action, - types.ChannelAdminLogEventActionExportedInviteEdit) + _tl.ChannelAdminLogEventActionExportedInviteEdit) @property def revoked_exported_invite(self): @@ -445,7 +445,7 @@ class AdminLogEvent: If `True`, `old` will be the revoked :tl:`ExportedChatInvite`. """ return isinstance(self.original.action, - types.ChannelAdminLogEventActionExportedInviteRevoke) + _tl.ChannelAdminLogEventActionExportedInviteRevoke) @property def joined_by_invite(self): @@ -456,7 +456,7 @@ class AdminLogEvent: used to join. """ return isinstance(self.original.action, - types.ChannelAdminLogEventActionParticipantJoinByInvite) + _tl.ChannelAdminLogEventActionParticipantJoinByInvite) @property def changed_user_volume(self): @@ -466,7 +466,7 @@ class AdminLogEvent: If `True`, `new` will be the updated :tl:`GroupCallParticipant`. """ return isinstance(self.original.action, - types.ChannelAdminLogEventActionParticipantVolume) + _tl.ChannelAdminLogEventActionParticipantVolume) def __str__(self): return str(self.original) diff --git a/telethon/types/_custom/button.py b/telethon/types/_custom/button.py index 4785271a..4dbcab99 100644 --- a/telethon/types/_custom/button.py +++ b/telethon/types/_custom/button.py @@ -49,12 +49,12 @@ class Button: Returns `True` if the button belongs to an inline keyboard. """ return isinstance(button, ( - types.KeyboardButtonBuy, - types.KeyboardButtonCallback, - types.KeyboardButtonGame, - types.KeyboardButtonSwitchInline, - types.KeyboardButtonUrl, - types.InputKeyboardButtonUrlAuth + _tl.KeyboardButtonBuy, + _tl.KeyboardButtonCallback, + _tl.KeyboardButtonGame, + _tl.KeyboardButtonSwitchInline, + _tl.KeyboardButtonUrl, + _tl.InputKeyboardButtonUrlAuth )) @staticmethod @@ -83,7 +83,7 @@ class Button: if len(data) > 64: raise ValueError('Too many bytes for the data') - return types.KeyboardButtonCallback(text, data) + return _tl.KeyboardButtonCallback(text, data) @staticmethod def switch_inline(text, query='', same_peer=False): @@ -101,7 +101,7 @@ class Button: input field will be filled with the username of your bot followed by the query text, ready to make inline queries. """ - return types.KeyboardButtonSwitchInline(text, query, same_peer) + return _tl.KeyboardButtonSwitchInline(text, query, same_peer) @staticmethod def url(text, url=None): @@ -117,7 +117,7 @@ class Button: the domain is trusted, and once confirmed the URL will open in their device. """ - return types.KeyboardButtonUrl(text, url or text) + return _tl.KeyboardButtonUrl(text, url or text) @staticmethod def auth(text, url=None, *, bot=None, write_access=False, fwd_text=None): @@ -157,10 +157,10 @@ class Button: When the user clicks this button, a confirmation box will be shown to the user asking whether they want to login to the specified domain. """ - return types.InputKeyboardButtonUrlAuth( + return _tl.InputKeyboardButtonUrlAuth( text=text, url=url or text, - bot=utils.get_input_user(bot or types.InputUserSelf()), + bot=utils.get_input_user(bot or _tl.InputUserSelf()), request_write_access=write_access, fwd_text=fwd_text ) @@ -191,7 +191,7 @@ class Button: between a button press and the user typing and sending exactly the same text on their own. """ - return cls(types.KeyboardButton(text), + return cls(_tl.KeyboardButton(text), resize=resize, single_use=single_use, selective=selective) @classmethod @@ -206,7 +206,7 @@ class Button: to the user asking whether they want to share their location with the bot, and if confirmed a message with geo media will be sent. """ - return cls(types.KeyboardButtonRequestGeoLocation(text), + return cls(_tl.KeyboardButtonRequestGeoLocation(text), resize=resize, single_use=single_use, selective=selective) @classmethod @@ -221,7 +221,7 @@ class Button: to the user asking whether they want to share their phone with the bot, and if confirmed a message with contact media will be sent. """ - return cls(types.KeyboardButtonRequestPhone(text), + return cls(_tl.KeyboardButtonRequestPhone(text), resize=resize, single_use=single_use, selective=selective) @classmethod @@ -243,7 +243,7 @@ class Button: When the user clicks this button, a screen letting the user create a poll will be shown, and if they do create one, the poll will be sent. """ - return cls(types.KeyboardButtonRequestPoll(text, quiz=force_quiz), + return cls(_tl.KeyboardButtonRequestPoll(text, quiz=force_quiz), resize=resize, single_use=single_use, selective=selective) @staticmethod @@ -255,7 +255,7 @@ class Button: ``selective`` is as documented in `text`. """ - return types.ReplyKeyboardHide(selective=selective) + return _tl.ReplyKeyboardHide(selective=selective) @staticmethod def force_reply(single_use=None, selective=None, placeholder=None): @@ -273,7 +273,7 @@ class Button: crop the text (for example, to 64 characters and adding an ellipsis (…) character as the 65th). """ - return types.ReplyKeyboardForceReply( + return _tl.ReplyKeyboardForceReply( single_use=single_use, selective=selective, placeholder=placeholder) @@ -291,7 +291,7 @@ class Button: `Payments API `__ documentation for more information. """ - return types.KeyboardButtonBuy(text) + return _tl.KeyboardButtonBuy(text) @staticmethod def game(text): @@ -305,4 +305,4 @@ class Button: `Games `__ documentation for more information on using games. """ - return types.KeyboardButtonGame(text) + return _tl.KeyboardButtonGame(text) diff --git a/telethon/types/_custom/inlineresult.py b/telethon/types/_custom/inlineresult.py index fa617af1..052be4dd 100644 --- a/telethon/types/_custom/inlineresult.py +++ b/telethon/types/_custom/inlineresult.py @@ -77,7 +77,7 @@ class InlineResult: this URL to open it in your browser, you should use Python's `webbrowser.open(url)` for such task. """ - if isinstance(self.result, types.BotInlineResult): + if isinstance(self.result, _tl.BotInlineResult): return self.result.url @property @@ -86,9 +86,9 @@ class InlineResult: Returns either the :tl:`WebDocument` thumbnail for normal results or the :tl:`Photo` for media results. """ - if isinstance(self.result, types.BotInlineResult): + if isinstance(self.result, _tl.BotInlineResult): return self.result.thumb - elif isinstance(self.result, types.BotInlineMediaResult): + elif isinstance(self.result, _tl.BotInlineMediaResult): return self.result.photo @property @@ -97,9 +97,9 @@ class InlineResult: Returns either the :tl:`WebDocument` content for normal results or the :tl:`Document` for media results. """ - if isinstance(self.result, types.BotInlineResult): + if isinstance(self.result, _tl.BotInlineResult): return self.result.content - elif isinstance(self.result, types.BotInlineMediaResult): + elif isinstance(self.result, _tl.BotInlineMediaResult): return self.result.document async def click(self, entity=None, reply_to=None, comment_to=None, diff --git a/telethon/types/_custom/participantpermissions.py b/telethon/types/_custom/participantpermissions.py index 6d4db912..7410aa88 100644 --- a/telethon/types/_custom/participantpermissions.py +++ b/telethon/types/_custom/participantpermissions.py @@ -46,8 +46,8 @@ class ParticipantPermissions: also counts as begin an administrator, since they have all permissions. """ return self.is_creator or isinstance(self.participant, ( - types.ChannelParticipantAdmin, - types.ChatParticipantAdmin + _tl.ChannelParticipantAdmin, + _tl.ChatParticipantAdmin )) @property @@ -56,8 +56,8 @@ class ParticipantPermissions: Whether the user is the creator of the chat or not. """ return isinstance(self.participant, ( - types.ChannelParticipantCreator, - types.ChatParticipantCreator + _tl.ChannelParticipantCreator, + _tl.ChatParticipantCreator )) @property @@ -67,9 +67,9 @@ class ParticipantPermissions: not banned either, and has no restrictions applied). """ return isinstance(self.participant, ( - types.ChannelParticipant, - types.ChatParticipant, - types.ChannelParticipantSelf + _tl.ChannelParticipant, + _tl.ChatParticipant, + _tl.ChannelParticipantSelf )) @property @@ -77,14 +77,14 @@ class ParticipantPermissions: """ Whether the user is banned in the chat. """ - return isinstance(self.participant, types.ChannelParticipantBanned) + return isinstance(self.participant, _tl.ChannelParticipantBanned) @property def has_left(self): """ Whether the user left the chat. """ - return isinstance(self.participant, types.ChannelParticipantLeft) + return isinstance(self.participant, _tl.ChannelParticipantLeft) @property def add_admins(self): diff --git a/telethon/types/_custom/qrlogin.py b/telethon/types/_custom/qrlogin.py index 3f2a0207..9a48884a 100644 --- a/telethon/types/_custom/qrlogin.py +++ b/telethon/types/_custom/qrlogin.py @@ -94,7 +94,7 @@ class QRLogin: async def handler(_update): event.set() - self._client.add_event_handler(handler, events.Raw(types.UpdateLoginToken)) + self._client.add_event_handler(handler, events.Raw(_tl.UpdateLoginToken)) try: # Will raise timeout error if it doesn't complete quick enough, @@ -105,12 +105,12 @@ class QRLogin: # We got here without it raising timeout error, so we can proceed resp = await self._client(self._request) - if isinstance(resp, types.auth.LoginTokenMigrateTo): + if isinstance(resp, _tl.auth.LoginTokenMigrateTo): await self._client._switch_dc(resp.dc_id) resp = await self._client(_tl.fn.auth.ImportLoginToken(resp.token)) # resp should now be auth.loginTokenSuccess - if isinstance(resp, types.auth.LoginTokenSuccess): + if isinstance(resp, _tl.auth.LoginTokenSuccess): user = resp.authorization.user self._client._on_login(user) return user From 3d36bb7b930af60705618066fe5a080c25bd46a5 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 18 Sep 2021 12:49:44 +0200 Subject: [PATCH 064/256] Change the way connection modes are specified --- readthedocs/misc/v2-migration-guide.rst | 30 +++++++++++++++++++++++++ telethon/__init__.py | 9 +------- telethon/_client/telegrambaseclient.py | 25 ++++++++++++++------- telethon/_client/telegramclient.py | 15 +++++++++---- telethon/_misc/enums.py | 22 ++++++++++++++++++ telethon/enums.py | 3 +++ 6 files changed, 84 insertions(+), 20 deletions(-) create mode 100644 telethon/_misc/enums.py create mode 100644 telethon/enums.py diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 4276dec8..8bf6ae6a 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -231,6 +231,36 @@ If you still want the old behaviour, wrap the list inside another list: #+ +Changes on how to configure a different connection mode +------------------------------------------------------- + +The ``connection`` parameter of the ``TelegramClient`` now expects a string, and not a type. +The supported values are: + +* ``'full'`` +* ``'intermediate'`` +* ``'abridged'`` +* ``'obfuscated'`` +* ``'http'`` + +The value chosen by the library is left as an implementation detail which may change. However, +you can force a certain mode by explicitly configuring it. If you don't want to hardcode the +string, you can import these values from the new ``telethon.enums`` module: + +.. code-block:: python + + client = TelegramClient(..., connection='tcp') + + # or + + from telethon.enums import ConnectionMode + client = TelegramClient(..., connection=ConnectionMode.TCP) + +You may have noticed there's currently no alternative for ``TcpMTProxy``. This mode has been +broken for some time now (see `issue #1319 `__) +anyway, so until there's a working solution, the mode is not supported. Pull Requests are welcome! + + The Conversation API has been removed ------------------------------------- diff --git a/telethon/__init__.py b/telethon/__init__.py index edbd4126..a10dc90c 100644 --- a/telethon/__init__.py +++ b/telethon/__init__.py @@ -5,13 +5,6 @@ from ._misc import utils # depends on helpers and _tl from ._misc import hints # depends on types/custom from ._client.telegramclient import TelegramClient -from ._network import connection -from . import version, events, utils, errors +from . import version, events, utils, errors, enums __version__ = version.__version__ - -__all__ = [ - 'TelegramClient', 'Button', - 'types', 'functions', 'custom', 'errors', - 'events', 'utils', 'connection' -] diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 35507edd..85c07057 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -9,8 +9,8 @@ import typing from .. import version, helpers, __name__ as __base_name__, _tl from .._crypto import rsa -from .._misc import markdown, entitycache, statecache -from .._network import MTProtoSender, Connection, ConnectionTcpFull, TcpMTProxy +from .._misc import markdown, entitycache, statecache, enums +from .._network import MTProtoSender, Connection, ConnectionTcpFull, connection as conns from ..sessions import Session, SQLiteSession, MemorySession DEFAULT_DC_ID = 2 @@ -191,10 +191,19 @@ def init( self._timeout = timeout self._auto_reconnect = auto_reconnect - assert isinstance(connection, type) - self._connection = connection - init_proxy = None if not issubclass(connection, TcpMTProxy) else \ - _tl.InputClientProxy(*connection.address_info(proxy)) + if connection == (): + # For now the current default remains TCP Full; may change to be "smart" if proxies are specified + connection = enums.ConnectionMode.FULL + + self._connection = { + enums.ConnectionMode.FULL: conns.ConnectionTcpFull, + enums.ConnectionMode.INTERMEDIATE: conns.ConnectionTcpIntermediate, + enums.ConnectionMode.ABRIDGED: conns.ConnectionTcpAbridged, + enums.ConnectionMode.OBFUSCATED: conns.ConnectionTcpObfuscated, + enums.ConnectionMode.HTTP: conns.ConnectionHttp, + }[enums.parse_conn_mode(connection)] + init_proxy = None if not issubclass(self._connection, conns.TcpMTProxy) else \ + _tl.InputClientProxy(*self._connection.address_info(proxy)) # Used on connection. Capture the variables in a lambda since # exporting clients need to create this InvokeWithLayer. @@ -334,7 +343,7 @@ async def disconnect(self: 'TelegramClient'): return await _disconnect_coro(self) def set_proxy(self: 'TelegramClient', proxy: typing.Union[tuple, dict]): - init_proxy = None if not issubclass(self._connection, TcpMTProxy) else \ + init_proxy = None if not issubclass(self._connection, conns.TcpMTProxy) else \ _tl.InputClientProxy(*self._connection.address_info(proxy)) self._init_request.proxy = init_proxy @@ -347,7 +356,7 @@ def set_proxy(self: 'TelegramClient', proxy: typing.Union[tuple, dict]): connection = getattr(self._sender, "_connection", None) if connection: - if isinstance(connection, TcpMTProxy): + if isinstance(connection, conns.TcpMTProxy): connection._ip = proxy[0] connection._port = proxy[1] else: diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index cb644dd2..796af317 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -12,6 +12,7 @@ from .. import helpers, version, _tl from ..types import _custom from .._network import ConnectionTcpFull from ..events.common import EventBuilder, EventCommon +from .._misc import enums class TelegramClient: @@ -37,9 +38,15 @@ class TelegramClient: api_hash (`str`): The API hash you obtained from https://my.telegram.org. - connection (`telethon.network.connection.common.Connection`, optional): - The connection instance to be used when creating a new connection - to the servers. It **must** be a type. + connection (`str`, optional): + The connection mode to be used when creating a new connection + to the servers. The available modes are: + + * ``'full'`` + * ``'intermediate'`` + * ``'abridged'`` + * ``'obfuscated'`` + * ``'http'`` Defaults to `telethon.network.connection.tcpfull.ConnectionTcpFull`. @@ -2768,7 +2775,7 @@ class TelegramClient: api_id: int, api_hash: str, *, - connection: 'typing.Type[Connection]' = ConnectionTcpFull, + connection: typing.Union[str, enums.ConnectionMode] = (), use_ipv6: bool = False, proxy: typing.Union[tuple, dict] = None, local_addr: typing.Union[str, tuple] = None, diff --git a/telethon/_misc/enums.py b/telethon/_misc/enums.py new file mode 100644 index 00000000..107bbc31 --- /dev/null +++ b/telethon/_misc/enums.py @@ -0,0 +1,22 @@ +from enum import Enum + + +class ConnectionMode(Enum): + FULL = 'full' + INTERMEDIATE = 'intermediate' + ABRIDGED = 'abridged' + OBFUSCATED = 'obfuscated' + HTTP = 'http' + + +def parse_conn_mode(mode): + if isinstance(mode, ConnectionMode): + return mode + elif isinstance(mode, str): + for cm in ConnectionMode: + if mode == cm.value: + return cm + + raise ValueError(f'unknown connection mode: {mode!r}') + else: + raise TypeError(f'not a valid connection mode: {type(mode).__name__!r}') diff --git a/telethon/enums.py b/telethon/enums.py new file mode 100644 index 00000000..42e588c0 --- /dev/null +++ b/telethon/enums.py @@ -0,0 +1,3 @@ +from ._misc.enums import ( + ConnectionMode, +) From 9af8ec8ccead54553d77b9c5cc2148fc8d3382cd Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 18 Sep 2021 13:04:13 +0200 Subject: [PATCH 065/256] Officially remove bot_file_id support --- readthedocs/misc/v2-migration-guide.rst | 17 +++ telethon/_client/downloads.py | 3 - telethon/_client/telegramclient.py | 4 - telethon/_client/uploads.py | 8 +- telethon/_misc/utils.py | 161 +----------------------- telethon/types/_custom/file.py | 15 --- 6 files changed, 20 insertions(+), 188 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 8bf6ae6a..51f161b5 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -178,6 +178,23 @@ The following modules have been moved inside ``_misc``: // TODO review telethon/__init__.py isn't exposing more than it should +Support for bot-API style file_id has been removed +-------------------------------------------------- + +They have been half-broken for a while now, so this is just making an existing reality official. +See `issue #1613 `__ for details. + +An alternative solution to re-use files may be provided in the future. For the time being, you +should either upload the file as needed, or keep a message with the media somewhere you can +later fetch it (by storing the chat and message identifier). + +Additionally, the ``custom.File.id`` property is gone (which used to provide access to this +"bot-API style" file identifier. + +// TODO could probably provide an in-memory cache for uploads to temporarily reuse old InputFile. +// this should lessen the impact of the removal of this feature + + The custom.Message class and the way it is used has changed ----------------------------------------------------------- diff --git a/telethon/_client/downloads.py b/telethon/_client/downloads.py index 6cd34b10..53e22cc1 100644 --- a/telethon/_client/downloads.py +++ b/telethon/_client/downloads.py @@ -282,9 +282,6 @@ async def download_media( date = datetime.datetime.now() media = message - if isinstance(media, str): - media = utils.resolve_bot_file_id(media) - if isinstance(media, _tl.MessageService): if isinstance(message.action, _tl.MessageActionChatEditPhoto): diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 796af317..b83369e9 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -3147,10 +3147,6 @@ class TelegramClient: send the file as "external" media, and Telegram is the one that will fetch the media and send it. - * A Bot API-like ``file_id``. You can convert previously - sent media to file IDs for later reusing with - `telethon.utils.pack_bot_file_id`. - * A handle to an existing file (for example, if you sent a message with media before, you can use its ``message.media`` as a file here). diff --git a/telethon/_client/uploads.py b/telethon/_client/uploads.py index 244c5b59..912954b0 100644 --- a/telethon/_client/uploads.py +++ b/telethon/_client/uploads.py @@ -425,17 +425,13 @@ async def _file_to_media( media = _tl.InputMediaPhotoExternal(file, ttl_seconds=ttl) else: media = _tl.InputMediaDocumentExternal(file, ttl_seconds=ttl) - else: - bot_file = utils.resolve_bot_file_id(file) - if bot_file: - media = utils.get_input_media(bot_file, ttl=ttl) if media: pass # Already have media, don't check the rest elif not file_handle: raise ValueError( - 'Failed to convert {} to media. Not an existing file, ' - 'an HTTP URL or a valid bot-API-like file ID'.format(file) + 'Failed to convert {} to media. Not an existing file or ' + 'HTTP URL'.format(file) ) elif as_image: media = _tl.InputMediaUploadedPhoto(file_handle, ttl_seconds=ttl) diff --git a/telethon/_misc/utils.py b/telethon/_misc/utils.py index 956a154d..8f4667b8 100644 --- a/telethon/_misc/utils.py +++ b/telethon/_misc/utils.py @@ -856,11 +856,7 @@ def is_image(file): """ Returns `True` if the file extension looks like an image file to Telegram. """ - match = re.match(r'\.(png|jpe?g)', _get_extension(file), re.IGNORECASE) - if match: - return True - else: - return isinstance(resolve_bot_file_id(file), _tl.Photo) + return bool(re.match(r'\.(png|jpe?g)', _get_extension(file), re.IGNORECASE)) def is_gif(file): @@ -1119,161 +1115,6 @@ def _encode_telegram_base64(string): return None # not valid base64, not valid ascii, not a string -def resolve_bot_file_id(file_id): - """ - Given a Bot API-style `file_id `, - returns the media it represents. If the `file_id ` - is not valid, `None` is returned instead. - - Note that the `file_id ` does not have information - such as image dimensions or file size, so these will be zero if present. - - For thumbnails, the photo ID and hash will always be zero. - """ - data = _rle_decode(_decode_telegram_base64(file_id)) - if not data: - return None - - # This isn't officially documented anywhere, but - # we assume the last byte is some kind of "version". - data, version = data[:-1], data[-1] - if version not in (2, 4): - return None - - if (version == 2 and len(data) == 24) or (version == 4 and len(data) == 25): - if version == 2: - file_type, dc_id, media_id, access_hash = struct.unpack(' Date: Sat, 18 Sep 2021 13:10:31 +0200 Subject: [PATCH 066/256] Officially remove resolve_invite_link --- readthedocs/misc/v2-migration-guide.rst | 13 +++++++++ telethon/_misc/utils.py | 37 ------------------------- telethon/sessions/memory.py | 4 --- 3 files changed, 13 insertions(+), 41 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 51f161b5..514aa8b8 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -195,6 +195,19 @@ Additionally, the ``custom.File.id`` property is gone (which used to provide acc // this should lessen the impact of the removal of this feature +Removal of several utility methods +---------------------------------- + +The following ``utils`` methods no longer exist or have been made private: + +* ``utils.resolve_bot_file_id``. It was half-broken. +* ``utils.pack_bot_file_id``. It was half-broken. +* ``utils.resolve_invite_link``. It has been broken for a while, so this just makes its removal + official (see `issue #1723 `__). + +// TODO provide the new clean utils + + The custom.Message class and the way it is used has changed ----------------------------------------------------------- diff --git a/telethon/_misc/utils.py b/telethon/_misc/utils.py index 8f4667b8..a92aff0b 100644 --- a/telethon/_misc/utils.py +++ b/telethon/_misc/utils.py @@ -1115,43 +1115,6 @@ def _encode_telegram_base64(string): return None # not valid base64, not valid ascii, not a string -def resolve_invite_link(link): - """ - Resolves the given invite link. Returns a tuple of - ``(link creator user id, global chat id, random int)``. - - Note that for broadcast channels or with the newest link format, the link - creator user ID will be zero to protect their identity. Normal chats and - megagroup channels will have such ID. - - Note that the chat ID may not be accurate for chats with a link that were - upgraded to megagroup, since the link can remain the same, but the chat - ID will be correct once a new link is generated. - """ - link_hash, is_link = parse_username(link) - if not is_link: - # Perhaps the user passed the link hash directly - link_hash = link - - # Little known fact, but invite links with a - # hex-string of bytes instead of base64 also works. - if re.match(r'[a-fA-F\d]+', link_hash) and len(link_hash) in (24, 32): - payload = bytes.fromhex(link_hash) - else: - payload = _decode_telegram_base64(link_hash) - - try: - if len(payload) == 12: - return (0, *struct.unpack('>LQ', payload)) - elif len(payload) == 16: - return struct.unpack('>LLQ', payload) - else: - pass - except (struct.error, TypeError): - pass - return None, None, None - - def resolve_inline_message_id(inline_msg_id): """ Resolves an inline message ID. Returns a tuple of diff --git a/telethon/sessions/memory.py b/telethon/sessions/memory.py index 82067101..d3d6e22a 100644 --- a/telethon/sessions/memory.py +++ b/telethon/sessions/memory.py @@ -204,10 +204,6 @@ class MemorySession(Session): username, invite = utils.parse_username(key) if username and not invite: result = self.get_entity_rows_by_username(username) - else: - tup = utils.resolve_invite_link(key)[1] - if tup: - result = self.get_entity_rows_by_id(tup, exact=False) elif isinstance(key, int): result = self.get_entity_rows_by_id(key, exact) From af81899bdc206596a9492df893e97255c918d0fb Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 18 Sep 2021 13:30:39 +0200 Subject: [PATCH 067/256] Don't automatically start the client via async-with --- readthedocs/misc/v2-migration-guide.rst | 32 +++++++++++++++++++++++++ telethon/_client/auth.py | 23 +++++++++++++++--- telethon/_client/telegramclient.py | 16 ++++++------- 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 514aa8b8..8c014498 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -178,6 +178,38 @@ The following modules have been moved inside ``_misc``: // TODO review telethon/__init__.py isn't exposing more than it should +Using the client in a context-manager no longer calls start automatically +------------------------------------------------------------------------- + + +The following code no longer automatically calls ``client.start()``: + +.. code-block:: python + + async with TelegramClient(...) as client: + ... + + # or + + async with client: + ... + + +This means the context-manager will only call ``client.connect()`` and ``client.disconnect()``. +The rationale for this change is that it could be strange for this to ask for the login code if +the session ever was invalid. If you want the old behaviour, you now need to be explicit: + + +.. code-block:: python + + async with TelegramClient(...).start() as client: + ... # ++++++++ + + +Note that you do not need to ``await`` the call to ``.start()`` if you are going to use the result +in a context-manager (but it's okay if you put the ``await``). + + Support for bot-API style file_id has been removed -------------------------------------------------- diff --git a/telethon/_client/auth.py b/telethon/_client/auth.py index 992d44e1..27ef4767 100644 --- a/telethon/_client/auth.py +++ b/telethon/_client/auth.py @@ -4,6 +4,7 @@ import os import sys import typing import warnings +import functools from .._misc import utils, helpers, password as pwd_mod from .. import errors, _tl @@ -13,7 +14,23 @@ if typing.TYPE_CHECKING: from .telegramclient import TelegramClient -async def start( +class StartingClient: + def __init__(self, client, start_fn): + self.client = client + self.start_fn = start_fn + + async def __aenter__(self): + await self.start_fn() + return self.client + + async def __aexit__(self, *args): + await self.client.__aexit__(*args) + + def __await__(self): + return self.__aenter__().__await__() + + +def start( self: 'TelegramClient', phone: typing.Callable[[], str] = lambda: input('Please enter your phone (or bot token): '), password: typing.Callable[[], str] = lambda: getpass.getpass('Please enter your password: '), @@ -40,7 +57,7 @@ async def start( raise ValueError('Both a phone and a bot token provided, ' 'must only provide one of either') - return await _start( + return StartingClient(self, functools.partial(_start, self=self, phone=phone, password=password, @@ -50,7 +67,7 @@ async def start( first_name=first_name, last_name=last_name, max_attempts=max_attempts - ) + )) async def _start( self: 'TelegramClient', phone, password, bot_token, force_sms, diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index b83369e9..b1c07a35 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -281,7 +281,7 @@ class TelegramClient: # region Auth - async def start( + def start( self: 'TelegramClient', phone: typing.Callable[[], str] = lambda: input('Please enter your phone (or bot token): '), password: typing.Callable[[], str] = lambda: getpass.getpass('Please enter your password: '), @@ -304,9 +304,8 @@ class TelegramClient: will be banned otherwise.** See https://telegram.org/tos and https://core.telegram.org/api/terms. - If the event loop is already running, this method returns a - coroutine that you should await on your own code; otherwise - the loop is ran until said coroutine completes. + Even though this method is not marked as ``async``, you still need to + ``await`` its result for it to do anything useful. Arguments phone (`str` | `int` | `callable`): @@ -363,11 +362,11 @@ class TelegramClient: # Please enter your password: ******* # (You are now logged in) - # Starting using a context manager (this calls start()): - with client: + # Starting using a context manager (note the lack of await): + async with client.start(): pass """ - return await auth.start(**locals()) + return auth.start(**locals()) async def sign_in( self: 'TelegramClient', @@ -621,7 +620,8 @@ class TelegramClient: return await auth.edit_2fa(**locals()) async def __aenter__(self): - return await self.start() + await self.connect() + return self async def __aexit__(self, *args): await self.disconnect() From 8114fb6c9b7bfa50737e8f9b083a1227e1c83a34 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 18 Sep 2021 13:34:21 +0200 Subject: [PATCH 068/256] Stop checking fwd_from or not out in message.edit --- readthedocs/misc/v2-migration-guide.rst | 11 +++++++++++ telethon/types/_custom/message.py | 6 ------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 8c014498..b28cf2bf 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -362,6 +362,17 @@ perform a separate request with no filter to fetch the total without filter (thi library used to do). +Using message.edit will now raise an error if the message cannot be edited +-------------------------------------------------------------------------- + +Before, calling ``message.edit()`` would completely ignore your attempt to edit a message if the +message had a forward header or was not outgoing. This is no longer the case. It is now the user's +responsibility to check for this. + +However, most likely, you were already doing the right thing (or else you would've experienced a +"why is this not being edited", which you would most likely consider a bug rather than a feature). + + The TelegramClient is no longer made out of mixins -------------------------------------------------- diff --git a/telethon/types/_custom/message.py b/telethon/types/_custom/message.py index 88a1a615..2d8f3fbe 100644 --- a/telethon/types/_custom/message.py +++ b/telethon/types/_custom/message.py @@ -762,9 +762,6 @@ class Message(ChatGetter, SenderGetter): `telethon.client.messages.MessageMethods.edit_message` with both ``entity`` and ``message`` already set. - Returns `None` if the message was incoming, - or the edited `Message` otherwise. - .. note:: This is different from `client.edit_message @@ -777,9 +774,6 @@ class Message(ChatGetter, SenderGetter): This is generally the most desired and convenient behaviour, and will work for link previews and message buttons. """ - if self.fwd_from or not self.out or not self._client: - return None # We assume self.out was patched for our chat - if 'link_preview' not in kwargs: kwargs['link_preview'] = bool(self.web_preview) From bf61dd32af45ad1a4a3139db4ea90ee4d889d7b5 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 18 Sep 2021 14:16:19 +0200 Subject: [PATCH 069/256] Change the way iter_participants filters are specified --- readthedocs/misc/v2-migration-guide.rst | 25 ++++++++++++++++-- telethon/_client/chats.py | 28 +++++++++++++------- telethon/_client/telegramclient.py | 18 ++++++++----- telethon/_misc/enums.py | 35 ++++++++++++++++++------- telethon/enums.py | 1 + 5 files changed, 80 insertions(+), 27 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index b28cf2bf..62a8bc30 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -240,6 +240,25 @@ The following ``utils`` methods no longer exist or have been made private: // TODO provide the new clean utils +Changes on how to configure filters for certain client methods +-------------------------------------------------------------- + +Before, ``client.iter_participants`` (and ``get_participants``) would expect a type or instance +of the raw Telegram definition as a ``filter``. Now, this ``filter`` expects a string. +The supported values are: + +* ``'admin'`` +* ``'bot'`` +* ``'kicked'`` +* ``'banned'`` +* ``'contact'`` + +If you prefer to avoid hardcoding strings, you may use ``telethon.enums.Participant``. + +// TODO maintain support for the old way of doing it? +// TODO now that there's a custom filter, filter client-side for small chats? + + The custom.Message class and the way it is used has changed ----------------------------------------------------------- @@ -345,12 +364,14 @@ actually using. Now it returns an ``int`` value indicating the number of message and were deleted. -The aggressive parameter hack has been removed ----------------------------------------------- +Changes to the methods to retrieve participants +----------------------------------------------- The "aggressive" hack in ``get_participants`` (and ``iter_participants``) is now gone. It was not reliable, and was a cause of flood wait errors. +The ``search`` parameter is no longer ignored when ``filter`` is specified. + The total value when getting participants has changed ----------------------------------------------------- diff --git a/telethon/_client/chats.py b/telethon/_client/chats.py index 1aa0724d..ffa06ac7 100644 --- a/telethon/_client/chats.py +++ b/telethon/_client/chats.py @@ -5,7 +5,7 @@ import string import typing from .. import hints, errors, _tl -from .._misc import helpers, utils, requestiter, tlobject +from .._misc import helpers, utils, requestiter, tlobject, enums from ..types import _custom if typing.TYPE_CHECKING: @@ -95,15 +95,25 @@ class _ChatAction: class _ParticipantsIter(requestiter.RequestIter): async def _init(self, entity, filter, search): - if isinstance(filter, type): - if filter in (_tl.ChannelParticipantsBanned, - _tl.ChannelParticipantsKicked, - _tl.ChannelParticipantsSearch, - _tl.ChannelParticipantsContacts): - # These require a `q` parameter (support types for convenience) - filter = filter('') + if not filter: + if search: + filter = _tl.ChannelParticipantsSearch(search) else: - filter = filter() + filter = _tl.ChannelParticipantsRecent() + else: + filter = enums.parse_participant(filter) + if filter == enums.Participant.ADMIN: + filter = _tl.ChannelParticipantsAdmins() + elif filter == enums.Participant.BOT: + filter = _tl.ChannelParticipantsBots() + elif filter == enums.Participant.KICKED: + filter = _tl.ChannelParticipantsKicked(search) + elif filter == enums.Participant.BANNED: + filter = _tl.ChannelParticipantsBanned(search) + elif filter == enums.Participant.CONTACT: + filter = _tl.ChannelParticipantsContacts(search) + else: + raise RuntimeError('unhandled enum variant') entity = await self.client.get_input_entity(entity) ty = helpers._entity_type(entity) diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index b1c07a35..839e1ea3 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -729,7 +729,7 @@ class TelegramClient: limit: float = (), *, search: str = '', - filter: '_tl.TypeChannelParticipantsFilter' = None) -> chats._ParticipantsIter: + filter: typing.Union[str, enums.Participant] = ()) -> chats._ParticipantsIter: """ Iterator over the participants belonging to the specified chat. @@ -749,16 +749,22 @@ class TelegramClient: search (`str`, optional): Look for participants with this string in name/username. - filter (:tl:`ChannelParticipantsFilter`, optional): + Note that the search is only compatible with some ``filter`` + when fetching members from a channel or megagroup. This may + change in the future. + + filter (`str`, optional): The filter to be used, if you want e.g. only admins Note that you might not have permissions for some filter. This has no effect for normal chats or users. - .. note:: + The available filters are: - The filter :tl:`ChannelParticipantsBanned` will return - *restricted* users. If you want *banned* users you should - use :tl:`ChannelParticipantsKicked` instead. + * ``'admin'`` + * ``'bot'`` + * ``'kicked'`` + * ``'banned'`` + * ``'contact'`` Yields The :tl:`User` objects returned by :tl:`GetParticipants` diff --git a/telethon/_misc/enums.py b/telethon/_misc/enums.py index 107bbc31..edce6776 100644 --- a/telethon/_misc/enums.py +++ b/telethon/_misc/enums.py @@ -9,14 +9,29 @@ class ConnectionMode(Enum): HTTP = 'http' -def parse_conn_mode(mode): - if isinstance(mode, ConnectionMode): - return mode - elif isinstance(mode, str): - for cm in ConnectionMode: - if mode == cm.value: - return cm +class Participant(Enum): + ADMIN = 'admin' + BOT = 'bot' + KICKED = 'kicked' + BANNED = 'banned' + CONTACT = 'contact' - raise ValueError(f'unknown connection mode: {mode!r}') - else: - raise TypeError(f'not a valid connection mode: {type(mode).__name__!r}') + +def _mk_parser(cls): + def parser(value): + if isinstance(value, cls): + return value + elif isinstance(value, str): + for variant in cls: + if value == variant.value: + return variant + + raise ValueError(f'unknown {cls.__name__}: {value!r}') + else: + raise TypeError(f'not a valid {cls.__name__}: {type(value).__name__!r}') + + return parser + + +parse_conn_mode = _mk_parser(ConnectionMode) +parse_participant = _mk_parser(Participant) diff --git a/telethon/enums.py b/telethon/enums.py index 42e588c0..8de39a15 100644 --- a/telethon/enums.py +++ b/telethon/enums.py @@ -1,3 +1,4 @@ from ._misc.enums import ( ConnectionMode, + Participant, ) From e524a74b8452f22ebbd6ba83d35084b40d2de494 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 18 Sep 2021 15:41:04 +0200 Subject: [PATCH 070/256] Remove client.disconnected property --- readthedocs/misc/v2-migration-guide.rst | 6 ++++ telethon/_client/telegrambaseclient.py | 3 -- telethon/_client/telegramclient.py | 38 ++++++------------------- telethon/_client/updates.py | 14 ++------- 4 files changed, 18 insertions(+), 43 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 62a8bc30..1ca03003 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -394,6 +394,12 @@ However, most likely, you were already doing the right thing (or else you would' "why is this not being edited", which you would most likely consider a bug rather than a feature). +The client.disconnected property has been removed +------------------------------------------------- + +``client.run_until_disconnected()`` should be used instead. + + The TelegramClient is no longer made out of mixins -------------------------------------------------- diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 85c07057..c8023429 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -301,9 +301,6 @@ def init( def get_loop(self: 'TelegramClient') -> asyncio.AbstractEventLoop: return asyncio.get_event_loop() -def get_disconnected(self: 'TelegramClient') -> asyncio.Future: - return self._sender.disconnected - def get_flood_sleep_threshold(self): return self._flood_sleep_threshold diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 839e1ea3..e2f1de76 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -2823,22 +2823,6 @@ class TelegramClient: """ return telegrambaseclient.get_loop(**locals()) - @property - def disconnected(self: 'TelegramClient') -> asyncio.Future: - """ - Property with a ``Future`` that resolves upon disconnection. - - Example - .. code-block:: python - - # Wait for a disconnection to occur - try: - await client.disconnected - except OSError: - print('Error on disconnect') - """ - return telegrambaseclient.get_disconnected(**locals()) - @property def flood_sleep_threshold(self): return telegrambaseclient.get_flood_sleep_threshold(**locals()) @@ -2928,30 +2912,26 @@ class TelegramClient: def run_until_disconnected(self: 'TelegramClient'): """ - Runs the event loop until the library is disconnected. + Wait until the library is disconnected. It also notifies Telegram that we want to receive updates as described in https://core.telegram.org/api/updates. + Event handlers will continue to run while the method awaits for a + disconnection to occur. Essentially, this method "blocks" until a + disconnection occurs, and keeps your code running if you have nothing + else to do. + Manual disconnections can be made by calling `disconnect() ` - or sending a ``KeyboardInterrupt`` (e.g. by pressing ``Ctrl+C`` on - the console window running the script). + or exiting the context-manager using the client (for example, a + ``KeyboardInterrupt`` by pressing ``Ctrl+C`` on the console window + would propagate the error, exit the ``with`` block and disconnect). If a disconnection error occurs (i.e. the library fails to reconnect automatically), said error will be raised through here, so you have a chance to ``except`` it on your own code. - If the loop is already running, this method returns a coroutine - that you should await on your own code. - - .. note:: - - If you want to handle ``KeyboardInterrupt`` in your code, - simply run the event loop in your code too in any way, such as - ``loop.run_forever()`` or ``await client.disconnected`` (e.g. - ``loop.run_until_complete(client.disconnected)``). - Example .. code-block:: python diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index 93e8a9bc..b319f8e2 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -18,23 +18,15 @@ if typing.TYPE_CHECKING: Callback = typing.Callable[[typing.Any], typing.Any] -async def _run_until_disconnected(self: 'TelegramClient'): - try: - # Make a high-level request to notify that we want updates - await self(_tl.fn.updates.GetState()) - return await self.disconnected - except KeyboardInterrupt: - pass - finally: - await self.disconnect() - async def set_receive_updates(self: 'TelegramClient', receive_updates): self._no_updates = not receive_updates if receive_updates: await self(_tl.fn.updates.GetState()) async def run_until_disconnected(self: 'TelegramClient'): - return await _run_until_disconnected(self) + # Make a high-level request to notify that we want updates + await self(_tl.fn.updates.GetState()) + return await self._sender.disconnected def on(self: 'TelegramClient', event: EventBuilder): def decorator(f): From 48c14df957c1fa521ca4ae2d3a2613369ef931e2 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 18 Sep 2021 16:05:07 +0200 Subject: [PATCH 071/256] Remove client.download_file --- readthedocs/misc/v2-migration-guide.rst | 10 ++- telethon/_client/downloads.py | 82 ++++++++++++++++--------- telethon/_client/telegramclient.py | 65 -------------------- 3 files changed, 62 insertions(+), 95 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 1ca03003..22bd2acd 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -181,7 +181,6 @@ The following modules have been moved inside ``_misc``: Using the client in a context-manager no longer calls start automatically ------------------------------------------------------------------------- - The following code no longer automatically calls ``client.start()``: .. code-block:: python @@ -210,6 +209,15 @@ Note that you do not need to ``await`` the call to ``.start()`` if you are going in a context-manager (but it's okay if you put the ``await``). +download_file has been removed from the client +---------------------------------------------- + +Instead, ``client.download_media`` should be used. + +The now-removed ``client.download_file`` method was a lower level implementation which should +have not been exposed at all. + + Support for bot-API style file_id has been removed -------------------------------------------------- diff --git a/telethon/_client/downloads.py b/telethon/_client/downloads.py index 53e22cc1..9ae1aec8 100644 --- a/telethon/_client/downloads.py +++ b/telethon/_client/downloads.py @@ -243,7 +243,12 @@ async def download_profile_photo( ) try: - result = await self.download_file(loc, file, dc_id=dc_id) + result = await _download_file( + self=self, + input_location=loc, + file=file, + dc_id=dc_id + ) return result if file is bytes else file except errors.LocationInvalidError: # See issue #500, Android app fails as of v4.6.0 (1155). @@ -308,29 +313,6 @@ async def download_media( self, media, file, progress_callback ) -async def download_file( - self: 'TelegramClient', - input_location: 'hints.FileLike', - file: 'hints.OutFileLike' = None, - *, - part_size_kb: float = None, - file_size: int = None, - progress_callback: 'hints.ProgressCallback' = None, - dc_id: int = None, - key: bytes = None, - iv: bytes = None) -> typing.Optional[bytes]: - return await _download_file( - self, - input_location, - file, - part_size_kb=part_size_kb, - file_size=file_size, - progress_callback=progress_callback, - dc_id=dc_id, - key=key, - iv=iv, - ) - async def _download_file( self: 'TelegramClient', input_location: 'hints.FileLike', @@ -343,6 +325,46 @@ async def _download_file( key: bytes = None, iv: bytes = None, msg_data: tuple = None) -> typing.Optional[bytes]: + """ + Low-level method to download files from their input location. + + Arguments + input_location (:tl:`InputFileLocation`): + The file location from which the file will be downloaded. + See `telethon.utils.get_input_location` source for a complete + list of supported _tl. + + file (`str` | `file`, optional): + The output file path, directory, or stream-like object. + If the path exists and is a file, it will be overwritten. + + If the file path is `None` or `bytes`, then the result + will be saved in memory and returned as `bytes`. + + part_size_kb (`int`, optional): + Chunk size when downloading files. The larger, the less + requests will be made (up to 512KB maximum). + + file_size (`int`, optional): + The file size that is about to be downloaded, if known. + Only used if ``progress_callback`` is specified. + + progress_callback (`callable`, optional): + A callback function accepting two parameters: + ``(downloaded bytes, total)``. Note that the + ``total`` is the provided ``file_size``. + + dc_id (`int`, optional): + The data center the library should connect to in order + to download the file. You shouldn't worry about this. + + key ('bytes', optional): + In case of an encrypted upload (secret chats) a key is supplied + + iv ('bytes', optional): + In case of an encrypted upload (secret chats) an iv is supplied + """ + if not part_size_kb: if not file_size: part_size_kb = 64 # Reasonable default @@ -568,14 +590,15 @@ async def _download_photo(self: 'TelegramClient', photo, file, date, thumb, prog else: file_size = size.size - result = await self.download_file( - _tl.InputPhotoFileLocation( + result = await _download_file( + self=self, + input_location=_tl.InputPhotoFileLocation( id=photo.id, access_hash=photo.access_hash, file_reference=photo.file_reference, thumb_size=size.type ), - file, + file=file, file_size=file_size, progress_callback=progress_callback ) @@ -626,13 +649,14 @@ async def _download_document( return _download_cached_photo_size(self, size, file) result = await _download_file( - _tl.InputDocumentFileLocation( + self=self, + input_location=_tl.InputDocumentFileLocation( id=document.id, access_hash=document.access_hash, file_reference=document.file_reference, thumb_size=size.type if size else '' ), - file, + file=file, file_size=size.size if size else document.size, progress_callback=progress_callback, msg_data=msg_data, diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index e2f1de76..811ad94b 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -1765,71 +1765,6 @@ class TelegramClient: """ return await downloads.download_media(**locals()) - async def download_file( - self: 'TelegramClient', - input_location: 'hints.FileLike', - file: 'hints.OutFileLike' = None, - *, - part_size_kb: float = None, - file_size: int = None, - progress_callback: 'hints.ProgressCallback' = None, - dc_id: int = None, - key: bytes = None, - iv: bytes = None) -> typing.Optional[bytes]: - """ - Low-level method to download files from their input location. - - .. note:: - - Generally, you should instead use `download_media`. - This method is intended to be a bit more low-level. - - Arguments - input_location (:tl:`InputFileLocation`): - The file location from which the file will be downloaded. - See `telethon.utils.get_input_location` source for a complete - list of supported _tl. - - file (`str` | `file`, optional): - The output file path, directory, or stream-like object. - If the path exists and is a file, it will be overwritten. - - If the file path is `None` or `bytes`, then the result - will be saved in memory and returned as `bytes`. - - part_size_kb (`int`, optional): - Chunk size when downloading files. The larger, the less - requests will be made (up to 512KB maximum). - - file_size (`int`, optional): - The file size that is about to be downloaded, if known. - Only used if ``progress_callback`` is specified. - - progress_callback (`callable`, optional): - A callback function accepting two parameters: - ``(downloaded bytes, total)``. Note that the - ``total`` is the provided ``file_size``. - - dc_id (`int`, optional): - The data center the library should connect to in order - to download the file. You shouldn't worry about this. - - key ('bytes', optional): - In case of an encrypted upload (secret chats) a key is supplied - - iv ('bytes', optional): - In case of an encrypted upload (secret chats) an iv is supplied - - - Example - .. code-block:: python - - # Download a file and print its header - data = await client.download_file(input_file, bytes) - print(data[:16]) - """ - return await downloads.download_file(**locals()) - def iter_download( self: 'TelegramClient', file: 'hints.FileLike', From 9f3bb52e4e61cd2af519c5320de2d2fd4000fb44 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 18 Sep 2021 16:10:01 +0200 Subject: [PATCH 072/256] Possibly fix _get_response_message for UpdateMessagePoll --- telethon/_client/messageparse.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/telethon/_client/messageparse.py b/telethon/_client/messageparse.py index 69d438fd..4cc17e15 100644 --- a/telethon/_client/messageparse.py +++ b/telethon/_client/messageparse.py @@ -140,7 +140,9 @@ def _get_response_message(self: 'TelegramClient', request, result, input_chat): media=_tl.MessageMediaPoll( poll=update.poll, results=update.results - ) + ), + date=None, + message='' ), entities, input_chat) if request is None: From 431a9309e392bef267507caabba3201078aab388 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 18 Sep 2021 16:29:45 +0200 Subject: [PATCH 073/256] Remove mark from peer_id --- readthedocs/misc/v2-migration-guide.rst | 26 ++++++++++++ telethon/_client/telegramclient.py | 6 +-- telethon/_client/users.py | 9 ++-- telethon/_misc/entitycache.py | 6 +-- telethon/_misc/utils.py | 51 +++-------------------- telethon/events/common.py | 9 +--- telethon/sessions/memory.py | 13 +----- telethon/sessions/sqlite.py | 12 +----- telethon_generator/generators/tlobject.py | 2 +- 9 files changed, 45 insertions(+), 89 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 22bd2acd..995a412e 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -31,6 +31,29 @@ 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. +Peer IDs, including chat_id and sender_id, no longer follow bot API conventions +------------------------------------------------------------------------------- + +Both the ``utils.get_peer_id`` and ``client.get_peer_id`` methods no longer have an ``add_mark`` +parameter. Both will always return the original ID as given by Telegram. This should lead to less +confusion. However, it also means that an integer ID on its own no longer embeds the information +about the type (did it belong to a user, chat, or channel?), so ``utils.get_peer`` can no longer +guess the type from just a number. + +Because it's not possible to know what other changes Telegram will do with identifiers, it's +probably best to get used to transparently storing whatever value they send along with the type +separatedly. + +As far as I can tell, user, chat and channel identifiers are globally unique, meaning a channel +and a user cannot share the same identifier. The library currently makes this assumption. However, +this is merely an observation (I have never heard of such a collision exist), and Telegram could +change at any time. If you want to be on the safe side, you're encouraged to save a pair of type +and identifier, rather than just the number. + +// TODO we DEFINITELY need to provide a way to "upgrade" old ids +// TODO and storing type+number by hand is a pain, provide better alternative + + Synchronous compatibility mode has been removed ----------------------------------------------- @@ -244,6 +267,9 @@ The following ``utils`` methods no longer exist or have been made private: * ``utils.pack_bot_file_id``. It was half-broken. * ``utils.resolve_invite_link``. It has been broken for a while, so this just makes its removal official (see `issue #1723 `__). +* ``utils.resolve_id``. Marked IDs are no longer used thorough the library. The removal of this + method also means ``utils.get_peer`` can no longer get a ``Peer`` from just a number, as the + type is no longer embedded inside the ID. // TODO provide the new clean utils diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 811ad94b..f3671e0b 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -3541,17 +3541,13 @@ class TelegramClient: async def get_peer_id( self: 'TelegramClient', - peer: 'hints.EntityLike', - add_mark: bool = True) -> int: + peer: 'hints.EntityLike') -> int: """ Gets the ID for the given entity. This method needs to be ``async`` because `peer` supports usernames, invite-links, phone numbers (from people in your contact list), etc. - If ``add_mark is False``, then a positive ID will be returned - instead. By default, bot-API style IDs (signed) are returned. - Example .. code-block:: python diff --git a/telethon/_client/users.py b/telethon/_client/users.py index f3393196..72786fe5 100644 --- a/telethon/_client/users.py +++ b/telethon/_client/users.py @@ -317,10 +317,9 @@ async def _get_peer(self: 'TelegramClient', peer: 'hints.EntityLike'): async def get_peer_id( self: 'TelegramClient', - peer: 'hints.EntityLike', - add_mark: bool = True) -> int: + peer: 'hints.EntityLike') -> int: if isinstance(peer, int): - return utils.get_peer_id(peer, add_mark=add_mark) + return utils.get_peer_id(peer) try: if peer.SUBCLASS_OF_ID not in (0x2d45687, 0xc91c90b6): @@ -332,7 +331,7 @@ async def get_peer_id( if isinstance(peer, _tl.InputPeerSelf): peer = await self.get_me(input_peer=True) - return utils.get_peer_id(peer, add_mark=add_mark) + return utils.get_peer_id(peer) async def _get_entity_from_string(self: 'TelegramClient', string): @@ -381,7 +380,7 @@ async def _get_entity_from_string(self: 'TelegramClient', string): .format(username)) from e try: - pid = utils.get_peer_id(result.peer, add_mark=False) + pid = utils.get_peer_id(result.peer) if isinstance(result.peer, _tl.PeerUser): return next(x for x in result.users if x.id == pid) else: diff --git a/telethon/_misc/entitycache.py b/telethon/_misc/entitycache.py index b6f87697..a191dc6f 100644 --- a/telethon/_misc/entitycache.py +++ b/telethon/_misc/entitycache.py @@ -119,12 +119,10 @@ class EntityCache: update.user_id not in dct: return False - if cid in has_chat_id and \ - utils.get_peer_id(_tl.PeerChat(update.chat_id)) not in dct: + if cid in has_chat_id and update.chat_id not in dct: return False - if cid in has_channel_id and \ - utils.get_peer_id(_tl.PeerChannel(update.channel_id)) not in dct: + if cid in has_channel_id and update.channel_id not in dct: return False if cid in has_peer and \ diff --git a/telethon/_misc/utils.py b/telethon/_misc/utils.py index a92aff0b..e412d563 100644 --- a/telethon/_misc/utils.py +++ b/telethon/_misc/utils.py @@ -961,10 +961,7 @@ def get_inner_text(text, entities): def get_peer(peer): try: - if isinstance(peer, int): - pid, cls = resolve_id(peer) - return cls(pid) - elif peer.SUBCLASS_OF_ID == 0x2d45687: + if peer.SUBCLASS_OF_ID == 0x2d45687: return peer elif isinstance(peer, ( _tl.contacts.ResolvedPeer, _tl.InputNotifyPeer, @@ -993,24 +990,13 @@ def get_peer(peer): _raise_cast_fail(peer, 'Peer') -def get_peer_id(peer, add_mark=True): +def get_peer_id(peer): """ - Convert the given peer into its marked ID by default. - - This "mark" comes from the "bot api" format, and with it the peer type - can be identified back. User ID is left unmodified, chat ID is negated, - and channel ID is "prefixed" with -100: - - * ``user_id`` - * ``-chat_id`` - * ``-100channel_id`` - - The original ID and the peer type class can be returned with - a call to :meth:`resolve_id(marked_id)`. + Extract the integer ID from the given peer. """ # First we assert it's a Peer TLObject, or early return for integers if isinstance(peer, int): - return peer if add_mark else resolve_id(peer)[0] + return peer # Tell the user to use their client to resolve InputPeerSelf if we got one if isinstance(peer, _tl.InputPeerSelf): @@ -1024,34 +1010,9 @@ def get_peer_id(peer, add_mark=True): if isinstance(peer, _tl.PeerUser): return peer.user_id elif isinstance(peer, _tl.PeerChat): - # Check in case the user mixed things up to avoid blowing up - if not (0 < peer.chat_id <= 0x7fffffff): - peer.chat_id = resolve_id(peer.chat_id)[0] - - return -peer.chat_id if add_mark else peer.chat_id + return peer.chat_id else: # if isinstance(peer, _tl.PeerChannel): - # Check in case the user mixed things up to avoid blowing up - if not (0 < peer.channel_id <= 0x7fffffff): - peer.channel_id = resolve_id(peer.channel_id)[0] - - if not add_mark: - return peer.channel_id - - # Growing backwards from -100_0000_000_000 indicates it's a channel - return -(1000000000000 + peer.channel_id) - - -def resolve_id(marked_id): - """Given a marked ID, returns the original ID and its :tl:`Peer` type.""" - if marked_id >= 0: - return marked_id, _tl.PeerUser - - marked_id = -marked_id - if marked_id > 1000000000000: - marked_id -= 1000000000000 - return marked_id, _tl.PeerChannel - else: - return marked_id, _tl.PeerChat + return peer.channel_id def _rle_decode(data): diff --git a/telethon/events/common.py b/telethon/events/common.py index 405537d2..f7b2e066 100644 --- a/telethon/events/common.py +++ b/telethon/events/common.py @@ -18,14 +18,7 @@ async def _into_id_set(client, chats): result = set() for chat in chats: if isinstance(chat, int): - if chat < 0: - result.add(chat) # Explicitly marked IDs are negative - else: - result.update({ # Support all valid types of peers - utils.get_peer_id(_tl.PeerUser(chat)), - utils.get_peer_id(_tl.PeerChat(chat)), - utils.get_peer_id(_tl.PeerChannel(chat)), - }) + result.add(chat) elif isinstance(chat, tlobject.TLObject) and chat.SUBCLASS_OF_ID == 0x2d45687: # 0x2d45687 == crc32(b'Peer') result.add(utils.get_peer_id(chat)) diff --git a/telethon/sessions/memory.py b/telethon/sessions/memory.py index d3d6e22a..5da811b2 100644 --- a/telethon/sessions/memory.py +++ b/telethon/sessions/memory.py @@ -165,17 +165,8 @@ class MemorySession(Session): def get_entity_rows_by_id(self, id, exact=True): try: - if exact: - return next((id, hash) for found_id, hash, _, _, _ - in self._entities if found_id == id) - else: - ids = ( - utils.get_peer_id(_tl.PeerUser(id)), - utils.get_peer_id(_tl.PeerChat(id)), - utils.get_peer_id(_tl.PeerChannel(id)) - ) - return next((id, hash) for found_id, hash, _, _, _ - in self._entities if found_id in ids) + return next((id, hash) for found_id, hash, _, _, _ + in self._entities if found_id == id) except StopIteration: pass diff --git a/telethon/sessions/sqlite.py b/telethon/sessions/sqlite.py index 5b4505c8..15fe5eaf 100644 --- a/telethon/sessions/sqlite.py +++ b/telethon/sessions/sqlite.py @@ -316,16 +316,8 @@ class SQLiteSession(MemorySession): 'select id, hash from entities where name = ?', name) def get_entity_rows_by_id(self, id, exact=True): - if exact: - return self._execute( - 'select id, hash from entities where id = ?', id) - else: - return self._execute( - 'select id, hash from entities where id in (?,?,?)', - utils.get_peer_id(_tl.PeerUser(id)), - utils.get_peer_id(_tl.PeerChat(id)), - utils.get_peer_id(_tl.PeerChannel(id)) - ) + return self._execute( + 'select id, hash from entities where id = ?', id) # File processing diff --git a/telethon_generator/generators/tlobject.py b/telethon_generator/generators/tlobject.py index 9cb43140..bb310c5a 100644 --- a/telethon_generator/generators/tlobject.py +++ b/telethon_generator/generators/tlobject.py @@ -32,7 +32,7 @@ AUTO_CASTS = { } NAMED_AUTO_CASTS = { - ('chat_id', 'int'): 'await client.get_peer_id({}, add_mark=False)' + ('chat_id', 'int'): 'await client.get_peer_id({})' } # Secret chats have a chat_id which may be negative. From 4321b97e9887f24b68b5807e371251e519ee51ca Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 18 Sep 2021 16:36:11 +0200 Subject: [PATCH 074/256] No longer run send_code_request from sign_in --- readthedocs/misc/v2-migration-guide.rst | 7 +++++++ telethon/_client/auth.py | 9 ++------- telethon/_client/telegramclient.py | 2 -- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 995a412e..b1f3a483 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -428,6 +428,13 @@ However, most likely, you were already doing the right thing (or else you would' "why is this not being edited", which you would most likely consider a bug rather than a feature). +Signing in no longer sends the code +----------------------------------- + +``client.sign_in()`` used to run ``client.send_code_request()`` if you only provided the phone and +not the code. It no longer does this. If you need that convenience, use ``client.start()`` instead. + + The client.disconnected property has been removed ------------------------------------------------- diff --git a/telethon/_client/auth.py b/telethon/_client/auth.py index 27ef4767..05dd05c3 100644 --- a/telethon/_client/auth.py +++ b/telethon/_client/auth.py @@ -225,9 +225,7 @@ async def sign_in( if me: return me - if phone and not code and not password: - return await self.send_code_request(phone) - elif code: + if phone and code: phone, phone_code_hash = \ _parse_phone_and_hash(self, phone, phone_code_hash) @@ -247,10 +245,7 @@ async def sign_in( api_id=self.api_id, api_hash=self.api_hash ) else: - raise ValueError( - 'You must provide a phone and a code the first time, ' - 'and a password only if an RPCError was raised before.' - ) + raise ValueError('You must provide either phone and code, password, or bot_token.') result = await self(request) if isinstance(result, _tl.auth.AuthorizationSignUpRequired): diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index f3671e0b..5683e81f 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -381,8 +381,6 @@ class TelegramClient: You should only use this if you are not authorized yet. - This method will send the code if it's not provided. - .. note:: In most cases, you should simply use `start()` and not this method. From 0b54fa7a25940132682ef3c3b26a5fdebeecd457 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 18 Sep 2021 16:54:54 +0200 Subject: [PATCH 075/256] Make edit_message parameters more consistent --- readthedocs/misc/v2-migration-guide.rst | 21 +++++++++++++++++++-- telethon/_client/messages.py | 12 ++---------- telethon/_client/telegramclient.py | 4 ++-- telethon/events/callbackquery.py | 2 +- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index b1f3a483..97cf43d3 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -417,8 +417,8 @@ perform a separate request with no filter to fetch the total without filter (thi library used to do). -Using message.edit will now raise an error if the message cannot be edited --------------------------------------------------------------------------- +Changes to editing messages +--------------------------- Before, calling ``message.edit()`` would completely ignore your attempt to edit a message if the message had a forward header or was not outgoing. This is no longer the case. It is now the user's @@ -427,6 +427,23 @@ responsibility to check for this. However, most likely, you were already doing the right thing (or else you would've experienced a "why is this not being edited", which you would most likely consider a bug rather than a feature). +When using ``client.edit_message``, you now must always specify the chat and the message (or +message identifier). This should be less "magic". As an example, if you were doing this before: + +.. code-block:: python + + await client.edit_message(message, 'new text') + +You now have to do the following: + +.. code-block:: python + + await client.edit_message(message.input_chat, message.id, 'new text') + + # or + + await message.edit('new text') + Signing in no longer sends the code ----------------------------------- diff --git a/telethon/_client/messages.py b/telethon/_client/messages.py index 5c073e60..e5290742 100644 --- a/telethon/_client/messages.py +++ b/telethon/_client/messages.py @@ -570,14 +570,6 @@ async def edit_message( supports_streaming: bool = False, schedule: 'hints.DateLike' = None ) -> '_tl.Message': - if isinstance(entity, _tl.InputBotInlineMessageID): - text = text or message - message = entity - elif isinstance(entity, _tl.Message): - text = message # Shift the parameters to the right - message = entity - entity = entity.peer_id - if formatting_entities is None: text, formatting_entities = await self._parse_message_text(text, parse_mode) file_handle, media, image = await self._file_to_media(file, @@ -586,9 +578,9 @@ async def edit_message( attributes=attributes, force_document=force_document) - if isinstance(entity, _tl.InputBotInlineMessageID): + if isinstance(message, _tl.InputBotInlineMessageID): request = _tl.fn.messages.EditInlineBotMessage( - id=entity, + id=message, message=text, no_webpage=not link_preview, entities=formatting_entities, diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 5683e81f..3d2360ad 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -2397,7 +2397,7 @@ class TelegramClient: async def edit_message( self: 'TelegramClient', entity: 'typing.Union[hints.EntityLike, _tl.Message]', - message: 'hints.MessageLike' = None, + message: 'hints.MessageLike', text: str = None, *, parse_mode: str = (), @@ -2519,7 +2519,7 @@ class TelegramClient: # or await client.edit_message(chat, message.id, 'hello!!') # or - await client.edit_message(message, 'hello!!!') + await message.edit('hello!!!') """ return await messages.edit_message(**locals()) diff --git a/telethon/events/callbackquery.py b/telethon/events/callbackquery.py index a100a947..0c944400 100644 --- a/telethon/events/callbackquery.py +++ b/telethon/events/callbackquery.py @@ -314,7 +314,7 @@ class CallbackQuery(EventBuilder): self._client.loop.create_task(self.answer()) if isinstance(self.query.msg_id, _tl.InputBotInlineMessageID): return await self._client.edit_message( - self.query.msg_id, *args, **kwargs + None, self.query.msg_id, *args, **kwargs ) else: return await self._client.edit_message( From 684f640b6071c0a609c9ac6f1fe1e4b3cdd14587 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 19 Sep 2021 13:45:19 +0200 Subject: [PATCH 076/256] Completely overhaul sessions --- readthedocs/misc/v2-migration-guide.rst | 66 ++++ telethon/sessions/abstract.py | 169 +++------- telethon/sessions/memory.py | 239 ++------------ telethon/sessions/sqlite.py | 394 ++++++++++-------------- telethon/sessions/string.py | 45 ++- 5 files changed, 339 insertions(+), 574 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 97cf43d3..f5e5fe90 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -73,6 +73,72 @@ removed. This implies: // TODO provide standalone alternative for this? +Complete overhaul of session files +---------------------------------- + +If you were using third-party libraries to deal with sessions, you will need to wait for those to +be updated. The library will automatically upgrade the SQLite session files to the new version, +and the ``StringSession`` remains backward-compatible. The sessions can now be async. + +In case you were relying on the tables used by SQLite (even though these should have been, and +will still need to be, treated as an implementation detail), here are the changes: + +* The ``sessions`` table is now correctly split into ``datacenter`` and ``session``. + ``datacenter`` contains information about a Telegram datacenter, along with its corresponding + authorization key, and ``session`` contains information about the update state and user. +* The ``entities`` table is now called ``entity`` and stores the ``type`` separatedly. +* The ``update_state`` table is now split into ``session`` and ``channel``, which can contain + a per-channel ``pts``. + +Because **the new version does not cache usernames, phone numbers and display names**, using these +in method calls is now quite expensive. You *should* migrate your code to do the Right Thing and +start using identifiers rather than usernames, phone numbers or invite links. This is both simpler +and more reliable, because while a user identifier won't change, their username could. + +You can use the following snippet to make a JSON backup (alternatively, you could just copy the +``.session`` file and keep it around) in case you want to preserve the cached usernames: + +.. code-block:: python + + import sqlite, json + with sqlite3.connect('your.session') as conn, open('entities.json', 'w', encoding='utf-8') as fp: + json.dump([ + {'id': id, 'hash': hash, 'username': username, 'phone': phone, 'name': name, 'date': date} + for (id, hash, username, phone, name, date) + in conn.execute('select id, hash, username, phone, name, date from entities') + ], fp) + +The following public methods or properties have also been removed from ``SQLiteSession`` because +they no longer make sense: + +* ``list_sessions``. You can ``glob.glob('*.session')`` instead. +* ``clone``. + +And the following, which were inherited from ``MemorySession``: + +* ``delete``. You can ``os.remove`` the file instead (preferably after ``client.log_out()``). +* ``set_dc``. +* ``dc_id``. +* ``server_address``. +* ``port``. +* ``auth_key``. +* ``takeout_id``. +* ``get_update_state``. +* ``set_update_state``. +* ``process_entities``. +* ``get_entity_rows_by_phone``. +* ``get_entity_rows_by_username``. +* ``get_entity_rows_by_name``. +* ``get_entity_rows_by_id``. +* ``get_input_entity``. +* ``cache_file``. +* ``get_file``. + +You also can no longer set ``client.session.save_entities = False``. The entities must be saved +for the library to work properly. If you still don't want it, you should subclass the session and +override the methods to do nothing. + + The "iter" variant of the client methods have been removed ---------------------------------------------------------- diff --git a/telethon/sessions/abstract.py b/telethon/sessions/abstract.py index 5fda1c18..4cdc9131 100644 --- a/telethon/sessions/abstract.py +++ b/telethon/sessions/abstract.py @@ -1,167 +1,90 @@ +from .types import DataCenter, ChannelState, SessionState, Entity + from abc import ABC, abstractmethod +from typing import List, Optional class Session(ABC): - def __init__(self): - pass - - def clone(self, to_instance=None): - """ - Creates a clone of this session file. - """ - return to_instance or self.__class__() - @abstractmethod - def set_dc(self, dc_id, server_address, port): + async def insert_dc(self, dc: DataCenter): """ - Sets the information of the data center address and port that - the library should connect to, as well as the data center ID, - which is currently unused. - """ - raise NotImplementedError - - @property - @abstractmethod - def dc_id(self): - """ - Returns the currently-used data center ID. - """ - raise NotImplementedError - - @property - @abstractmethod - def server_address(self): - """ - Returns the server address where the library should connect to. - """ - raise NotImplementedError - - @property - @abstractmethod - def port(self): - """ - Returns the port to which the library should connect to. - """ - raise NotImplementedError - - @property - @abstractmethod - def auth_key(self): - """ - Returns an ``AuthKey`` instance associated with the saved - data center, or `None` if a new one should be generated. - """ - raise NotImplementedError - - @auth_key.setter - @abstractmethod - def auth_key(self, value): - """ - Sets the ``AuthKey`` to be used for the saved data center. - """ - raise NotImplementedError - - @property - @abstractmethod - def takeout_id(self): - """ - Returns an ID of the takeout process initialized for this session, - or `None` if there's no were any unfinished takeout requests. - """ - raise NotImplementedError - - @takeout_id.setter - @abstractmethod - def takeout_id(self, value): - """ - Sets the ID of the unfinished takeout process for this session. + Store a new or update an existing `DataCenter` with matching ``id``. """ raise NotImplementedError @abstractmethod - def get_update_state(self, entity_id): + async def get_all_dc(self) -> List[DataCenter]: """ - Returns the ``UpdateState`` associated with the given `entity_id`. - If the `entity_id` is 0, it should return the ``UpdateState`` for - no specific channel (the "general" state). If no state is known - it should ``return None``. + Get a list of all currently-stored `DataCenter`. Should not contain duplicate ``id``. """ raise NotImplementedError @abstractmethod - def set_update_state(self, entity_id, state): + async def set_state(self, state: SessionState): """ - Sets the given ``UpdateState`` for the specified `entity_id`, which - should be 0 if the ``UpdateState`` is the "general" state (and not - for any specific channel). + Set the state about the current session. """ raise NotImplementedError @abstractmethod - def close(self): + async def get_state(self) -> Optional[SessionState]: """ - Called on client disconnection. Should be used to - free any used resources. Can be left empty if none. - """ - - @abstractmethod - def save(self): - """ - Called whenever important properties change. It should - make persist the relevant session information to disk. + Get the state about the current session. """ raise NotImplementedError @abstractmethod - def delete(self): + async def insert_channel_state(self, state: ChannelState): """ - Called upon client.log_out(). Should delete the stored - information from disk since it's not valid anymore. - """ - raise NotImplementedError - - @classmethod - def list_sessions(cls): - """ - Lists available sessions. Not used by the library itself. - """ - return [] - - @abstractmethod - def process_entities(self, tlo): - """ - Processes the input ``TLObject`` or ``list`` and saves - whatever information is relevant (e.g., ID or access hash). + Store a new or update an existing `ChannelState` with matching ``id``. """ raise NotImplementedError @abstractmethod - def get_input_entity(self, key): + async def get_all_channel_states(self) -> List[ChannelState]: """ - Turns the given key into an ``InputPeer`` (e.g. ``InputPeerUser``). - The library uses this method whenever an ``InputPeer`` is needed - to suit several purposes (e.g. user only provided its ID or wishes - to use a cached username to avoid extra RPC). + Get a list of all currently-stored `ChannelState`. Should not contain duplicate ``id``. """ raise NotImplementedError @abstractmethod - def cache_file(self, md5_digest, file_size, instance): + async def insert_entities(self, entities: List[Entity]): """ - Caches the given file information persistently, so that it - doesn't need to be re-uploaded in case the file is used again. + Store new or update existing `Entity` with matching ``id``. - The ``instance`` will be either an ``InputPhoto`` or ``InputDocument``, - both with an ``.id`` and ``.access_hash`` attributes. + Entities should be saved on a best-effort. It is okay to not save them, although the + library may need to do extra work if a previously-saved entity is missing, or even be + unable to continue without the entity. """ raise NotImplementedError @abstractmethod - def get_file(self, md5_digest, file_size, cls): + async def get_entity(self, ty: int, id: int) -> Optional[Entity]: """ - Returns an instance of ``cls`` if the ``md5_digest`` and ``file_size`` - match an existing saved record. The class will either be an - ``InputPhoto`` or ``InputDocument``, both with two parameters - ``id`` and ``access_hash`` in that order. + Get the `Entity` with matching ``ty`` and ``id``. + + The following groups of ``ty`` should be treated to be equivalent, that is, for a given + ``ty`` and ``id``, if the ``ty`` is in a given group, a matching ``access_hash`` with + that ``id`` from within any ``ty`` in that group should be returned. + + * ``'U'`` and ``'B'`` (user and bot). + * ``'G'`` (small group chat). + * ``'C'``, ``'M'`` and ``'E'`` (broadcast channel, megagroup channel, and gigagroup channel). + + For example, if a ``ty`` representing a bot is stored but the asking ``ty`` is a user, + the corresponding ``access_hash`` should still be returned. + + You may use `types.canonical_entity_type` to find out the canonical type. + """ + raise NotImplementedError + + @abstractmethod + async def save(self): + """ + Save the session. + + May do nothing if the other methods already saved when they were called. + + May return custom data when manual saving is intended. """ raise NotImplementedError diff --git a/telethon/sessions/memory.py b/telethon/sessions/memory.py index 5da811b2..67602ec9 100644 --- a/telethon/sessions/memory.py +++ b/telethon/sessions/memory.py @@ -1,230 +1,47 @@ -from enum import Enum - +from .types import DataCenter, ChannelState, SessionState, Entity from .abstract import Session from .._misc import utils, tlobject from .. import _tl - -class _SentFileType(Enum): - DOCUMENT = 0 - PHOTO = 1 - - @staticmethod - def from_type(cls): - if cls == _tl.InputDocument: - return _SentFileType.DOCUMENT - elif cls == _tl.InputPhoto: - return _SentFileType.PHOTO - else: - raise ValueError('The cls must be either InputDocument/InputPhoto') +from typing import List, Optional class MemorySession(Session): + __slots__ = ('dcs', 'state', 'channel_states', 'entities') + def __init__(self): - super().__init__() + self.dcs = {} + self.state = None + self.channel_states = {} + self.entities = {} - self._dc_id = 0 - self._server_address = None - self._port = None - self._auth_key = None - self._takeout_id = None + async def insert_dc(self, dc: DataCenter): + self.dcs[dc.id] = dc - self._files = {} - self._entities = set() - self._update_states = {} + async def get_all_dc(self) -> List[DataCenter]: + return list(self.dcs.values()) - def set_dc(self, dc_id, server_address, port): - self._dc_id = dc_id or 0 - self._server_address = server_address - self._port = port + async def set_state(self, state: SessionState): + self.state = state - @property - def dc_id(self): - return self._dc_id + async def get_state(self) -> Optional[SessionState]: + return self.state - @property - def server_address(self): - return self._server_address + async def insert_channel_state(self, state: ChannelState): + self.channel_states[state.channel_id] = state - @property - def port(self): - return self._port + async def get_all_channel_states(self) -> List[ChannelState]: + return list(self.channel_states.values()) - @property - def auth_key(self): - return self._auth_key + async def insert_entities(self, entities: List[Entity]): + self.entities.update((e.id, (e.ty, e.access_hash)) for e in entities) - @auth_key.setter - def auth_key(self, value): - self._auth_key = value - - @property - def takeout_id(self): - return self._takeout_id - - @takeout_id.setter - def takeout_id(self, value): - self._takeout_id = value - - def get_update_state(self, entity_id): - return self._update_states.get(entity_id, None) - - def set_update_state(self, entity_id, state): - self._update_states[entity_id] = state - - def close(self): - pass - - def save(self): - pass - - def delete(self): - pass - - @staticmethod - def _entity_values_to_row(id, hash, username, phone, name): - # While this is a simple implementation it might be overrode by, - # other classes so they don't need to implement the plural form - # of the method. Don't remove. - return id, hash, username, phone, name - - def _entity_to_row(self, e): - if not isinstance(e, tlobject.TLObject): - return + async def get_entity(self, ty: int, id: int) -> Optional[Entity]: try: - p = utils.get_input_peer(e, allow_self=False) - marked_id = utils.get_peer_id(p) - except TypeError: - # Note: `get_input_peer` already checks for non-zero `access_hash`. - # See issues #354 and #392. It also checks that the entity - # is not `min`, because its `access_hash` cannot be used - # anywhere (since layer 102, there are two access hashes). - return - - if isinstance(p, (_tl.InputPeerUser, _tl.InputPeerChannel)): - p_hash = p.access_hash - elif isinstance(p, _tl.InputPeerChat): - p_hash = 0 - else: - return - - username = getattr(e, 'username', None) or None - if username is not None: - username = username.lower() - phone = getattr(e, 'phone', None) - name = utils.get_display_name(e) or None - return self._entity_values_to_row( - marked_id, p_hash, username, phone, name - ) - - def _entities_to_rows(self, tlo): - if not isinstance(tlo, tlobject.TLObject) and utils.is_list_like(tlo): - # This may be a list of users already for instance - entities = tlo - else: - entities = [] - if hasattr(tlo, 'user'): - entities.append(tlo.user) - if hasattr(tlo, 'chat'): - entities.append(tlo.chat) - if hasattr(tlo, 'chats') and utils.is_list_like(tlo.chats): - entities.extend(tlo.chats) - if hasattr(tlo, 'users') and utils.is_list_like(tlo.users): - entities.extend(tlo.users) - - rows = [] # Rows to add (id, hash, username, phone, name) - for e in entities: - row = self._entity_to_row(e) - if row: - rows.append(row) - return rows - - def process_entities(self, tlo): - self._entities |= set(self._entities_to_rows(tlo)) - - def get_entity_rows_by_phone(self, phone): - try: - return next((id, hash) for id, hash, _, found_phone, _ - in self._entities if found_phone == phone) - except StopIteration: - pass - - def get_entity_rows_by_username(self, username): - try: - return next((id, hash) for id, hash, found_username, _, _ - in self._entities if found_username == username) - except StopIteration: - pass - - def get_entity_rows_by_name(self, name): - try: - return next((id, hash) for id, hash, _, _, found_name - in self._entities if found_name == name) - except StopIteration: - pass - - def get_entity_rows_by_id(self, id, exact=True): - try: - return next((id, hash) for found_id, hash, _, _, _ - in self._entities if found_id == id) - except StopIteration: - pass - - def get_input_entity(self, key): - try: - if key.SUBCLASS_OF_ID in (0xc91c90b6, 0xe669bf46, 0x40f202fd): - # hex(crc32(b'InputPeer', b'InputUser' and b'InputChannel')) - # We already have an Input version, so nothing else required - return key - # Try to early return if this key can be casted as input peer - return utils.get_input_peer(key) - except (AttributeError, TypeError): - # Not a TLObject or can't be cast into InputPeer - if isinstance(key, tlobject.TLObject): - key = utils.get_peer_id(key) - exact = True - else: - exact = not isinstance(key, int) or key < 0 - - result = None - if isinstance(key, str): - phone = utils.parse_phone(key) - if phone: - result = self.get_entity_rows_by_phone(phone) - else: - username, invite = utils.parse_username(key) - if username and not invite: - result = self.get_entity_rows_by_username(username) - - elif isinstance(key, int): - result = self.get_entity_rows_by_id(key, exact) - - if not result and isinstance(key, str): - result = self.get_entity_rows_by_name(key) - - if result: - entity_id, entity_hash = result # unpack resulting tuple - entity_id, kind = utils.resolve_id(entity_id) - # removes the mark and returns type of entity - if kind == _tl.PeerUser: - return _tl.InputPeerUser(entity_id, entity_hash) - elif kind == _tl.PeerChat: - return _tl.InputPeerChat(entity_id) - elif kind == _tl.PeerChannel: - return _tl.InputPeerChannel(entity_id, entity_hash) - else: - raise ValueError('Could not find input entity with key ', key) - - def cache_file(self, md5_digest, file_size, instance): - if not isinstance(instance, (_tl.InputDocument, _tl.InputPhoto)): - raise TypeError('Cannot cache %s instance' % type(instance)) - key = (md5_digest, file_size, _SentFileType.from_type(type(instance))) - value = (instance.id, instance.access_hash) - self._files[key] = value - - def get_file(self, md5_digest, file_size, cls): - key = (md5_digest, file_size, _SentFileType.from_type(cls)) - try: - return cls(*self._files[key]) + ty, access_hash = self.entities[id] + return Entity(ty, id, access_hash) except KeyError: return None + + async def save(self): + pass diff --git a/telethon/sessions/sqlite.py b/telethon/sessions/sqlite.py index 15fe5eaf..5cd288aa 100644 --- a/telethon/sessions/sqlite.py +++ b/telethon/sessions/sqlite.py @@ -1,11 +1,13 @@ import datetime import os import time +import ipaddress +from typing import Optional, List -from .memory import MemorySession, _SentFileType +from .abstract import Session from .._misc import utils from .. import _tl -from .._crypto import AuthKey +from .types import DataCenter, ChannelState, SessionState, Entity try: import sqlite3 @@ -15,16 +17,17 @@ except ImportError as e: sqlite3_err = type(e) EXTENSION = '.session' -CURRENT_VERSION = 7 # database version +CURRENT_VERSION = 8 # database version -class SQLiteSession(MemorySession): - """This session contains the required information to login into your - Telegram account. NEVER give the saved session file to anyone, since - they would gain instant access to all your messages and contacts. +class SQLiteSession(Session): + """ + This session contains the required information to login into your + 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 - through an official Telegram client to revoke the authorization. + If you think the session has been compromised, close all the sessions + through an official Telegram client to revoke the authorization. """ def __init__(self, session_id=None): @@ -53,66 +56,13 @@ class SQLiteSession(MemorySession): c.execute("delete from version") c.execute("insert into version values (?)", (CURRENT_VERSION,)) self.save() - - # These values will be saved - c.execute('select * from sessions') - tuple_ = c.fetchone() - if tuple_: - self._dc_id, self._server_address, self._port, key, \ - self._takeout_id = tuple_ - self._auth_key = AuthKey(data=key) - - c.close() else: # Tables don't exist, create new ones - self._create_table( - c, - "version (version integer primary key)" - , - """sessions ( - dc_id integer primary key, - server_address text, - port integer, - auth_key blob, - takeout_id integer - )""" - , - """entities ( - id integer primary key, - hash integer not null, - username text, - phone integer, - name text, - date integer - )""" - , - """sent_files ( - md5_digest blob, - file_size integer, - type integer, - id integer, - hash integer, - primary key(md5_digest, file_size, type) - )""" - , - """update_state ( - id integer primary key, - pts integer, - qts integer, - date integer, - seq integer - )""" - ) + self._mk_tables(c) c.execute("insert into version values (?)", (CURRENT_VERSION,)) - self._update_session_table() c.close() self.save() - def clone(self, to_instance=None): - cloned = super().clone(to_instance) - cloned.save_entities = self.save_entities - return cloned - def _upgrade_database(self, old): c = self._cursor() if old == 1: @@ -150,75 +100,164 @@ class SQLiteSession(MemorySession): if old == 6: old += 1 c.execute("alter table entities add column date integer") + if old == 7: + self._mk_tables(c) + c.execute(''' + insert into datacenter (id, ip, port, auth) + select dc_id, server_address, port, auth_key + from sessions + ''') + c.execute(''' + insert into session (user_id, dc_id, bot, pts, qts, date, seq, takeout_id) + select + 0, + s.dc_id, + 0, + coalesce(u.pts, 0), + coalesce(u.qts, 0), + coalesce(u.date, 0), + coalesce(u.seq, 0), + s.takeout_id + from sessions s + left join update_state u on u.id = 0 + limit 1 + ''') + c.execute(''' + insert into entity (id, access_hash, ty) + select + case + when id < -1000000000000 then -(id + 1000000000000) + when id < 0 then -id + else id + end, + hash, + case + when id < -1000000000000 then 67 + when id < 0 then 71 + else 85 + end + from entities + ''') + c.execute('drop table sessions') + c.execute('drop table entities') + c.execute('drop table sent_files') + c.execute('drop table update_state') - c.close() + def _mk_tables(self, c): + self._create_table( + c, + '''version ( + version integer primary key + )''', + '''datacenter ( + id integer primary key, + ip text not null, + port integer not null, + auth blob not null + )''', + '''session ( + user_id integer primary key, + dc_id integer not null, + bot integer not null, + pts integer not null, + qts integer not null, + date integer not null, + seq integer not null, + takeout_id integer + )''', + '''channel ( + channel_id integer primary key, + pts integer not null + )''', + '''entity ( + id integer primary key, + access_hash integer not null, + ty integer not null + )''', + ) + + async def insert_dc(self, dc: DataCenter): + self._execute( + 'insert or replace into datacenter values (?,?,?,?)', + dc.id, + str(ipaddress.ip_address(dc.ipv6 or dc.ipv4)), + dc.port, + dc.auth + ) + + async def get_all_dc(self) -> List[DataCenter]: + c = self._cursor() + res = [] + for (id, ip, port, auth) in c.execute('select * from datacenter'): + ip = ipaddress.ip_address(ip) + res.append(DataCenter( + id=id, + ipv4=int(ip) if ip.version == 4 else None, + ipv6=int(ip) if ip.version == 6 else None, + port=port, + auth=auth, + )) + return res + + async def set_state(self, state: SessionState): + self._execute( + 'insert or replace into session values (?,?,?,?,?,?,?,?)', + state.user_id, + state.dc_id, + int(state.bot), + state.pts, + state.qts, + state.date, + state.seq, + state.takeout_id, + ) + + async def get_state(self) -> Optional[SessionState]: + row = self._execute('select * from session') + return SessionState(*row) if row else None + + async def insert_channel_state(self, state: ChannelState): + self._execute( + 'insert or replace into channel values (?,?)', + state.channel_id, + state.pts, + ) + + async def get_all_channel_states(self) -> List[ChannelState]: + c = self._cursor() + try: + return [ + ChannelState(*row) + for row in c.execute('select * from channel') + ] + finally: + c.close() + + async def insert_entities(self, entities: List[Entity]): + c = self._cursor() + try: + c.executemany( + 'insert or replace into entity values (?,?,?)', + [(e.id, e.access_hash, e.ty) for e in entities] + ) + finally: + c.close() + + async def get_entity(self, ty: int, id: int) -> Optional[Entity]: + row = self._execute('select ty, id, access_hash from entity where id = ?', id) + return Entity(*row) if row else None + + async def save(self): + # This is a no-op if there are no changes to commit, so there's + # no need for us to keep track of an "unsaved changes" variable. + if self._conn is not None: + self._conn.commit() @staticmethod def _create_table(c, *definitions): for definition in definitions: c.execute('create table {}'.format(definition)) - # Data from sessions should be kept as properties - # not to fetch the database every time we need it - def set_dc(self, dc_id, server_address, port): - super().set_dc(dc_id, server_address, port) - self._update_session_table() - - # Fetch the auth_key corresponding to this data center - row = self._execute('select auth_key from sessions') - if row and row[0]: - self._auth_key = AuthKey(data=row[0]) - else: - self._auth_key = None - - @MemorySession.auth_key.setter - def auth_key(self, value): - self._auth_key = value - self._update_session_table() - - @MemorySession.takeout_id.setter - def takeout_id(self, value): - self._takeout_id = value - self._update_session_table() - - def _update_session_table(self): - c = self._cursor() - # While we can save multiple rows into the sessions table - # currently we only want to keep ONE as the tables don't - # tell us which auth_key's are usable and will work. Needs - # some more work before being able to save auth_key's for - # multiple DCs. Probably done differently. - c.execute('delete from sessions') - c.execute('insert or replace into sessions values (?,?,?,?,?)', ( - self._dc_id, - self._server_address, - self._port, - self._auth_key.key if self._auth_key else b'', - self._takeout_id - )) - c.close() - - def get_update_state(self, entity_id): - row = self._execute('select pts, qts, date, seq from update_state ' - 'where id = ?', entity_id) - if row: - pts, qts, date, seq = row - date = datetime.datetime.fromtimestamp( - date, tz=datetime.timezone.utc) - return _tl.updates.State(pts, qts, date, seq, unread_count=0) - - def set_update_state(self, entity_id, state): - self._execute('insert or replace into update_state values (?,?,?,?,?)', - entity_id, state.pts, state.qts, - state.date.timestamp(), state.seq) - - def save(self): - """Saves the current session object as session_user_id.session""" - # This is a no-op if there are no changes to commit, so there's - # no need for us to keep track of an "unsaved changes" variable. - if self._conn is not None: - self._conn.commit() - def _cursor(self): """Asserts that the connection is open and returns a cursor""" if self._conn is None: @@ -236,108 +275,3 @@ class SQLiteSession(MemorySession): return c.execute(stmt, values).fetchone() finally: c.close() - - def close(self): - """Closes the connection unless we're working in-memory""" - if self.filename != ':memory:': - if self._conn is not None: - self._conn.commit() - self._conn.close() - self._conn = None - - def delete(self): - """Deletes the current session file""" - if self.filename == ':memory:': - return True - try: - os.remove(self.filename) - return True - except OSError: - return False - - @classmethod - def list_sessions(cls): - """Lists all the sessions of the users who have ever connected - using this client and never logged out - """ - return [os.path.splitext(os.path.basename(f))[0] - for f in os.listdir('.') if f.endswith(EXTENSION)] - - # Entity processing - - def process_entities(self, tlo): - """ - Processes all the found entities on the given TLObject, - unless .save_entities is False. - """ - if not self.save_entities: - return - - rows = self._entities_to_rows(tlo) - if not rows: - return - - c = self._cursor() - try: - now_tup = (int(time.time()),) - rows = [row + now_tup for row in rows] - c.executemany( - 'insert or replace into entities values (?,?,?,?,?,?)', rows) - finally: - c.close() - - def get_entity_rows_by_phone(self, phone): - return self._execute( - 'select id, hash from entities where phone = ?', phone) - - def get_entity_rows_by_username(self, username): - c = self._cursor() - try: - results = c.execute( - 'select id, hash, date from entities where username = ?', - (username,) - ).fetchall() - - if not results: - return None - - # If there is more than one result for the same username, evict the oldest one - if len(results) > 1: - results.sort(key=lambda t: t[2] or 0) - c.executemany('update entities set username = null where id = ?', - [(t[0],) for t in results[:-1]]) - - return results[-1][0], results[-1][1] - finally: - c.close() - - def get_entity_rows_by_name(self, name): - return self._execute( - 'select id, hash from entities where name = ?', name) - - def get_entity_rows_by_id(self, id, exact=True): - return self._execute( - 'select id, hash from entities where id = ?', id) - - # File processing - - def get_file(self, md5_digest, file_size, cls): - row = self._execute( - 'select id, hash from sent_files ' - 'where md5_digest = ? and file_size = ? and type = ?', - md5_digest, file_size, _SentFileType.from_type(cls).value - ) - if row: - # Both allowed classes have (id, access_hash) as parameters - return cls(row[0], row[1]) - - def cache_file(self, md5_digest, file_size, instance): - if not isinstance(instance, (_tl.InputDocument, _tl.InputPhoto)): - raise TypeError('Cannot cache %s instance' % type(instance)) - - self._execute( - 'insert or replace into sent_files values (?,?,?,?,?)', - md5_digest, file_size, - _SentFileType.from_type(type(instance)).value, - instance.id, instance.access_hash - ) diff --git a/telethon/sessions/string.py b/telethon/sessions/string.py index 72617f24..2cb66aa6 100644 --- a/telethon/sessions/string.py +++ b/telethon/sessions/string.py @@ -4,7 +4,7 @@ import struct from .abstract import Session from .memory import MemorySession -from .._crypto import AuthKey +from .types import DataCenter, ChannelState, SessionState, Entity _STRUCT_PREFORMAT = '>B{}sH256s' @@ -34,12 +34,33 @@ class StringSession(MemorySession): string = string[1:] ip_len = 4 if len(string) == 352 else 16 - self._dc_id, ip, self._port, key = struct.unpack( + dc_id, ip, port, key = struct.unpack( _STRUCT_PREFORMAT.format(ip_len), StringSession.decode(string)) - self._server_address = ipaddress.ip_address(ip).compressed - if any(key): - self._auth_key = AuthKey(key) + self.state = SessionState( + dc_id=dc_id, + user_id=0, + bot=False, + pts=0, + qts=0, + date=0, + seq=0, + takeout_id=0 + ) + if ip_len == 4: + ipv4 = int.from_bytes(ip, 'big', False) + ipv6 = None + else: + ipv4 = None + ipv6 = int.from_bytes(ip, 'big', signed=False) + + self.dcs[dc_id] = DataCenter( + id=dc_id, + ipv4=ipv4, + ipv6=ipv6, + port=port, + auth=key + ) @staticmethod def encode(x: bytes) -> str: @@ -50,14 +71,18 @@ class StringSession(MemorySession): return base64.urlsafe_b64decode(x) def save(self: Session): - if not self.auth_key: + if not self.state: return '' - ip = ipaddress.ip_address(self.server_address).packed + if self.state.ipv6 is not None: + ip = self.state.ipv6.to_bytes(16, 'big', signed=False) + else: + ip = self.state.ipv6.to_bytes(4, 'big', signed=False) + return CURRENT_VERSION + StringSession.encode(struct.pack( _STRUCT_PREFORMAT.format(len(ip)), - self.dc_id, + self.state.dc_id, ip, - self.port, - self.auth_key.key + self.state.port, + self.dcs[self.state.dc_id].auth )) From 29d3c3fd7c26beb743463dd431237d8b01542d30 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 19 Sep 2021 15:56:28 +0200 Subject: [PATCH 077/256] Fix outdated LAYER usage in _create_exported_sender --- telethon/_client/telegrambaseclient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index c8023429..f9541e44 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -480,7 +480,7 @@ async def _create_exported_sender(self: 'TelegramClient', dc_id): self._log[__name__].info('Exporting auth for new borrowed sender in %s', dc) auth = await self(_tl.fn.auth.ExportAuthorization(dc_id)) self._init_request.query = _tl.fn.auth.ImportAuthorization(id=auth.id, bytes=auth.bytes) - req = _tl.fn.InvokeWithLayer(LAYER, self._init_request) + req = _tl.fn.InvokeWithLayer(_tl.LAYER, self._init_request) await sender.send(req) return sender From 1f5722c925441200fa6b52a91d3487a5cb38cdf8 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 19 Sep 2021 16:37:53 +0200 Subject: [PATCH 078/256] Add missing session/types file --- telethon/sessions/types.py | 156 +++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 telethon/sessions/types.py diff --git a/telethon/sessions/types.py b/telethon/sessions/types.py new file mode 100644 index 00000000..39033a1f --- /dev/null +++ b/telethon/sessions/types.py @@ -0,0 +1,156 @@ +from typing import Optional, Tuple + + +class DataCenter: + """ + Stores the information needed to connect to a datacenter. + + * id: 32-bit number representing the datacenter identifier as given by Telegram. + * ipv4 and ipv6: 32-bit or 128-bit number storing the IP address of the datacenter. + * port: 16-bit number storing the port number needed to connect to the datacenter. + * bytes: arbitrary binary payload needed to authenticate to the datacenter. + """ + __slots__ = ('id', 'ipv4', 'ipv6', 'port', 'auth') + + def __init__( + self, + id: int, + ipv4: Optional[int], + ipv6: Optional[int], + port: int, + auth: bytes + ): + self.id = id + self.ipv4 = ipv4 + self.ipv6 = ipv6 + self.port = port + self.auth = auth + + +class SessionState: + """ + Stores the information needed to fetch updates and about the current user. + + * user_id: 64-bit number representing the user identifier. + * dc_id: 32-bit number relating to the datacenter identifier where the user is. + * bot: is the logged-in user a bot? + * pts: 64-bit number holding the state needed to fetch updates. + * qts: alternative 64-bit number holding the state needed to fetch updates. + * date: 64-bit number holding the date needed to fetch updates. + * seq: 64-bit-number holding the sequence number needed to fetch updates. + * takeout_id: 64-bit-number holding the identifier of the current takeout session. + + Note that some of the numbers will only use 32 out of the 64 available bits. + However, for future-proofing reasons, we recommend you pretend they are 64-bit long. + """ + __slots__ = ('user_id', 'dc_id', 'bot', 'pts', 'qts', 'date', 'seq', 'takeout_id') + + def __init__( + self, + user_id: int, + dc_id: int, + bot: bool, + pts: int, + qts: int, + date: int, + seq: int, + takeout_id: Optional[int], + ): + self.user_id = user_id + self.dc_id = dc_id + self.bot = bot + self.pts = pts + self.qts = qts + self.date = date + self.seq = seq + + +class ChannelState: + """ + Stores the information needed to fetch updates from a channel. + + * channel_id: 64-bit number representing the channel identifier. + * pts: 64-bit number holding the state needed to fetch updates. + """ + __slots__ = ('channel_id', 'pts') + + def __init__( + self, + channel_id: int, + pts: int + ): + self.channel_id = channel_id + self.pts = pts + + +class Entity: + """ + Stores the information needed to use a certain user, chat or channel with the API. + + * ty: 8-bit number indicating the type of the entity. + * id: 64-bit number uniquely identifying the entity among those of the same type. + * access_hash: 64-bit number needed to use this entity with the API. + + You can rely on the ``ty`` value to be equal to the ASCII character one of: + + * 'U' (85): this entity belongs to a :tl:`User` who is not a ``bot``. + * 'B' (66): this entity belongs to a :tl:`User` who is a ``bot``. + * 'G' (71): this entity belongs to a small group :tl:`Chat`. + * 'C' (67): this entity belongs to a standard broadcast :tl:`Channel`. + * 'M' (77): this entity belongs to a megagroup :tl:`Channel`. + * 'E' (69): this entity belongs to an "enormous" "gigagroup" :tl:`Channel`. + """ + __slots__ = ('ty', 'id', 'access_hash') + + USER = ord('U') + BOT = ord('B') + GROUP = ord('G') + CHANNEL = ord('C') + MEGAGROUP = ord('M') + GIGAGROUP = ord('E') + + def __init__( + self, + ty: int, + id: int, + access_hash: int + ): + self.ty = ty + self.id = id + self.access_hash = access_hash + + +def canonical_entity_type(ty: int, *, _mapping={ + Entity.USER: Entity.USER, + Entity.BOT: Entity.USER, + Entity.GROUP: Entity.GROUP, + Entity.CHANNEL: Entity.CHANNEL, + Entity.MEGAGROUP: Entity.CHANNEL, + Entity.GIGAGROUP: Entity.CHANNEL, +}) -> int: + """ + Return the canonical version of an entity type. + """ + try: + return _mapping[ty] + except KeyError: + ty = chr(ty) if isinstance(ty, int) else ty + raise ValueError(f'entity type {ty!r} is not valid') + + +def get_entity_type_group(ty: int, *, _mapping={ + Entity.USER: (Entity.USER, Entity.BOT), + Entity.BOT: (Entity.USER, Entity.BOT), + Entity.GROUP: (Entity.GROUP,), + Entity.CHANNEL: (Entity.CHANNEL, Entity.MEGAGROUP, Entity.GIGAGROUP), + Entity.MEGAGROUP: (Entity.CHANNEL, Entity.MEGAGROUP, Entity.GIGAGROUP), + Entity.GIGAGROUP: (Entity.CHANNEL, Entity.MEGAGROUP, Entity.GIGAGROUP), +}) -> Tuple[int]: + """ + Return the group where an entity type belongs to. + """ + try: + return _mapping[ty] + except KeyError: + ty = chr(ty) if isinstance(ty, int) else ty + raise ValueError(f'entity type {ty!r} is not valid') From 81b4957d9bf6d0ee69530bc6eaa0ae13b315d5a8 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 19 Sep 2021 16:38:11 +0200 Subject: [PATCH 079/256] Update code to deal with the new sessions --- readthedocs/misc/v2-migration-guide.rst | 1 + telethon/_client/auth.py | 1 - telethon/_client/downloads.py | 23 ++-- telethon/_client/messages.py | 5 +- telethon/_client/telegrambaseclient.py | 134 +++++++++++++++--------- telethon/_client/telegramclient.py | 6 +- telethon/_client/updates.py | 20 ++-- telethon/_client/users.py | 31 +++--- telethon/_misc/entitycache.py | 54 ++++++++-- telethon/_network/mtprotosender.py | 17 +-- 10 files changed, 173 insertions(+), 119 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index f5e5fe90..a2c32f70 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -117,6 +117,7 @@ they no longer make sense: And the following, which were inherited from ``MemorySession``: * ``delete``. You can ``os.remove`` the file instead (preferably after ``client.log_out()``). + ``client.log_out()`` also no longer deletes the session file (it can't as there's no method). * ``set_dc``. * ``dc_id``. * ``server_address``. diff --git a/telethon/_client/auth.py b/telethon/_client/auth.py index 05dd05c3..296656f7 100644 --- a/telethon/_client/auth.py +++ b/telethon/_client/auth.py @@ -372,7 +372,6 @@ async def log_out(self: 'TelegramClient') -> bool: self._state_cache.reset() await self.disconnect() - self.session.delete() return True async def edit_2fa( diff --git a/telethon/_client/downloads.py b/telethon/_client/downloads.py index 9ae1aec8..99932cdc 100644 --- a/telethon/_client/downloads.py +++ b/telethon/_client/downloads.py @@ -39,26 +39,17 @@ class _DirectDownloadIter(requestiter.RequestIter): self._msg_data = msg_data self._timed_out = False - self._exported = dc_id and self.client.session.dc_id != dc_id + # TODO should cache current session state + state = await self.client.session.get_state() + + self._exported = dc_id and state.dc_id != dc_id if not self._exported: # The used sender will also change if ``FileMigrateError`` occurs self._sender = self.client._sender else: - try: - self._sender = await self.client._borrow_exported_sender(dc_id) - except errors.DcIdInvalidError: - # Can't export a sender for the ID we are currently in - config = await self.client(_tl.fn.help.GetConfig()) - for option in config.dc_options: - if option.ip_address == self.client.session.server_address: - self.client.session.set_dc( - option.id, option.ip_address, option.port) - self.client.session.save() - break - - # TODO Figure out why the session may have the wrong DC ID - self._sender = self.client._sender - self._exported = False + # If this raises DcIdInvalidError, it means we tried exporting the same DC we're in. + # This should not happen, but if it does, it's a bug. + self._sender = await self.client._borrow_exported_sender(dc_id) async def _load_next_chunk(self): cur = await self._request() diff --git a/telethon/_client/messages.py b/telethon/_client/messages.py index e5290742..69f00a0c 100644 --- a/telethon/_client/messages.py +++ b/telethon/_client/messages.py @@ -589,7 +589,10 @@ async def edit_message( ) # Invoke `messages.editInlineBotMessage` from the right datacenter. # Otherwise, Telegram will error with `MESSAGE_ID_INVALID` and do nothing. - exported = self.session.dc_id != entity.dc_id + # TODO should cache current session state + state = await self.session.get_state() + + exported = state.dc_id != entity.dc_id if exported: try: sender = await self._borrow_exported_sender(entity.dc_id) diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index f9541e44..1050c2a5 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -6,12 +6,14 @@ import logging import platform import time import typing +import ipaddress from .. import version, helpers, __name__ as __base_name__, _tl from .._crypto import rsa from .._misc import markdown, entitycache, statecache, enums from .._network import MTProtoSender, Connection, ConnectionTcpFull, connection as conns from ..sessions import Session, SQLiteSession, MemorySession +from ..sessions.types import DataCenter, SessionState DEFAULT_DC_ID = 2 DEFAULT_IPV4_IP = '149.154.167.51' @@ -129,15 +131,6 @@ def init( 'The given session must be a str or a Session instance.' ) - # ':' in session.server_address is True if it's an IPv6 address - if (not session.server_address or - (':' in session.server_address) != use_ipv6): - session.set_dc( - DEFAULT_DC_ID, - DEFAULT_IPV6_IP if self._use_ipv6 else DEFAULT_IPV4_IP, - DEFAULT_PORT - ) - self.flood_sleep_threshold = flood_sleep_threshold # TODO Use AsyncClassWrapper(session) @@ -230,13 +223,11 @@ def init( ) self._sender = MTProtoSender( - self.session.auth_key, loggers=self._log, retries=self._connection_retries, delay=self._retry_delay, auto_reconnect=self._auto_reconnect, connect_timeout=self._timeout, - auth_key_callback=self._auth_key_callback, update_callback=self._handle_update, auto_reconnect_callback=self._handle_auto_reconnect ) @@ -264,11 +255,6 @@ def init( self._authorized = None # None = unknown, False = no, True = yes - # Update state (for catching up after a disconnection) - # TODO Get state from channels too - self._state_cache = statecache.StateCache( - self.session.get_update_state(0), self._log) - # Some further state for subclasses self._event_builders = [] @@ -310,10 +296,33 @@ def set_flood_sleep_threshold(self, value): async def connect(self: 'TelegramClient') -> None: + all_dc = await self.session.get_all_dc() + state = await self.session.get_state() + + dc = None + if state: + for d in all_dc: + if d.id == state.dc_id: + dc = d + break + + if dc is None: + dc = DataCenter( + id=DEFAULT_DC_ID, + ipv4=None if self._use_ipv6 else int(ipaddress.ip_address(DEFAULT_IPV4_IP)), + ipv6=int(ipaddress.ip_address(DEFAULT_IPV6_IP)) if self._use_ipv6 else None, + port=DEFAULT_PORT, + auth=b'', + ) + + # Update state (for catching up after a disconnection) + # TODO Get state from channels too + self._state_cache = statecache.StateCache(state, self._log) + if not await self._sender.connect(self._connection( - self.session.server_address, - self.session.port, - self.session.dc_id, + str(ipaddress.ip_address(dc.ipv6 or dc.ipv4)), + dc.port, + dc.id, loggers=self._log, proxy=self._proxy, local_addr=self._local_addr @@ -321,8 +330,10 @@ async def connect(self: 'TelegramClient') -> None: # We don't want to init or modify anything if we were already connected return - self.session.auth_key = self._sender.auth_key - self.session.save() + if self._sender.auth_key.key != dc.key: + dc.key = self._sender.auth_key.key + await self.session.insert_dc(dc) + await self.session.save() self._init_request.query = _tl.fn.help.GetConfig() @@ -388,15 +399,12 @@ async def _disconnect_coro(self: 'TelegramClient'): pts, date = self._state_cache[None] if pts and date: - self.session.set_update_state(0, _tl.updates.State( - pts=pts, - qts=0, - date=date, - seq=0, - unread_count=0 - )) - - self.session.close() + state = await self.session.get_state() + if state: + state.pts = pts + state.date = date + await self.session.set_state(state) + await self.session.save() async def _disconnect(self: 'TelegramClient'): """ @@ -414,31 +422,59 @@ async def _switch_dc(self: 'TelegramClient', new_dc): Permanently switches the current connection to the new data center. """ self._log[__name__].info('Reconnecting to new data center %s', new_dc) - dc = await _get_dc(self, new_dc) + dc = await _refresh_and_get_dc(self, new_dc) + + state = await self.session.get_state() + if state is None: + state = SessionState( + user_id=0, + dc_id=dc.id, + bot=False, + pts=0, + qts=0, + date=0, + seq=0, + takeout_id=None, + ) + else: + state.dc_id = dc.id + + await self.session.set_state(dc.id) + await self.session.save() - self.session.set_dc(dc.id, dc.ip_address, dc.port) - # auth_key's are associated with a server, which has now changed - # so it's not valid anymore. Set to None to force recreating it. - self._sender.auth_key.key = None - self.session.auth_key = None - self.session.save() await _disconnect(self) return await self.connect() -def _auth_key_callback(self: 'TelegramClient', auth_key): - """ - Callback from the sender whenever it needed to generate a - new authorization key. This means we are not authorized. - """ - self.session.auth_key = auth_key - self.session.save() +async def _refresh_and_get_dc(self: 'TelegramClient', dc_id): + """ + Gets the Data Center (DC) associated to `dc_id`. -async def _get_dc(self: 'TelegramClient', dc_id): - """Gets the Data Center (DC) associated to 'dc_id'""" + Also take this opportunity to refresh the addresses stored in the session if needed. + """ cls = self.__class__ if not cls._config: cls._config = await self(_tl.fn.help.GetConfig()) + all_dc = {dc.id: dc for dc in await self.session.get_all_dc()} + for dc in cls._config.dc_options: + if dc.media_only or dc.tcpo_only or dc.cdn: + continue + + ip = int(ipaddress.ip_address(dc.ip_address)) + if dc.id in all_dc: + all_dc[dc.id].port = dc.port + if dc.ipv6: + all_dc[dc.id].ipv6 = ip + else: + all_dc[dc.id].ipv4 = ip + elif dc.ipv6: + all_dc[dc.id] = DataCenter(dc.id, None, ip, dc.port, b'') + else: + all_dc[dc.id] = DataCenter(dc.id, ip, None, dc.port, b'') + + for dc in all_dc.values(): + await self.session.insert_dc(dc) + await self.session.save() try: return next( @@ -463,12 +499,12 @@ async def _create_exported_sender(self: 'TelegramClient', dc_id): """ # Thanks badoualy/kotlogram on /telegram/api/DefaultTelegramClient.kt # for clearly showing how to export the authorization - dc = await _get_dc(self, dc_id) + dc = await _refresh_and_get_dc(self, dc_id) # Can't reuse self._sender._connection as it has its own seqno. # # If one were to do that, Telegram would reset the connection # with no further clues. - sender = MTProtoSender(None, loggers=self._log) + sender = MTProtoSender(loggers=self._log) await sender.connect(self._connection( dc.ip_address, dc.port, @@ -503,7 +539,7 @@ async def _borrow_exported_sender(self: 'TelegramClient', dc_id): self._borrowed_senders[dc_id] = (state, sender) elif state.need_connect(): - dc = await _get_dc(self, dc_id) + dc = await _refresh_and_get_dc(self, dc_id) await sender.connect(self._connection( dc.ip_address, dc.port, diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 3d2360ad..e5dc6f8f 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -206,8 +206,7 @@ class TelegramClient: 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 - `. + preserved for future usage in the session. Arguments finalize (`bool`): @@ -3599,9 +3598,6 @@ class TelegramClient: async def _clean_exported_senders(self: 'TelegramClient'): return await telegrambaseclient._clean_exported_senders(**locals()) - def _auth_key_callback(self: 'TelegramClient', auth_key): - return telegrambaseclient._auth_key_callback - def _handle_update(self: 'TelegramClient', update): return updates._handle_update(**locals()) diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index b319f8e2..0ae8b299 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -79,7 +79,7 @@ async def catch_up(self: 'TelegramClient'): if not pts: return - self.session.catching_up = True + self._catching_up = True try: while True: d = await self(_tl.fn.updates.GetDifference( @@ -129,16 +129,13 @@ async def catch_up(self: 'TelegramClient'): finally: # TODO Save new pts to session self._state_cache._pts_date = (pts, date) - self.session.catching_up = False + self._catching_up = False # It is important to not make _handle_update async because we rely on # the order that the updates arrive in to update the pts and date to # be always-increasing. There is also no need to make this async. def _handle_update(self: 'TelegramClient', update): - self.session.process_entities(update) - self._entity_cache.add(update) - if isinstance(update, (_tl.Updates, _tl.UpdatesCombined)): entities = {utils.get_peer_id(x): x for x in itertools.chain(update.users, update.chats)} @@ -203,11 +200,10 @@ async def _update_loop(self: 'TelegramClient'): except (ConnectionError, asyncio.CancelledError): return - # Entities and cached files are not saved when they are - # inserted because this is a rather expensive operation - # (default's sqlite3 takes ~0.1s to commit changes). Do - # it every minute instead. No-op if there's nothing new. - self.session.save() + # Entities are not saved when they are inserted because this is a rather expensive + # operation (default's sqlite3 takes ~0.1s to commit changes). Do it every minute + # instead. No-op if there's nothing new. + await self.session.save() # We need to send some content-related request at least hourly # for Telegram to keep delivering updates, otherwise they will @@ -232,6 +228,10 @@ async def _dispatch_queue_updates(self: 'TelegramClient'): self._dispatching_updates_queue.clear() async def _dispatch_update(self: 'TelegramClient', update, others, channel_id, pts_date): + entities = self._entity_cache.add(list((update._entities or {}).values())) + if entities: + await self.session.insert_entities(entities) + if not self._entity_cache.ensure_cached(update): # We could add a lock to not fetch the same pts twice if we are # already fetching it. However this does not happen in practice, diff --git a/telethon/_client/users.py b/telethon/_client/users.py index 72786fe5..394baee9 100644 --- a/telethon/_client/users.py +++ b/telethon/_client/users.py @@ -7,6 +7,7 @@ import typing from .. import errors, hints, _tl from .._misc import helpers, utils from ..errors import MultiError, RPCError +from ..sessions.types import Entity _NOT_A_REQUEST = lambda: TypeError('You can only invoke requests, not types!') @@ -70,8 +71,9 @@ async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sl exceptions.append(e) results.append(None) continue - self.session.process_entities(result) - self._entity_cache.add(result) + entities = self._entity_cache.add(result) + if entities: + await self.session.insert_entities(entities) exceptions.append(None) results.append(result) request_index += 1 @@ -81,8 +83,9 @@ async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sl return results else: result = await future - self.session.process_entities(result) - self._entity_cache.add(result) + entities = self._entity_cache.add(result) + if entities: + await self.session.insert_entities(entities) return result except (errors.ServerError, errors.RpcCallFailError, errors.RpcMcgetFailError, errors.InterdcCallErrorError, @@ -266,9 +269,19 @@ async def get_input_entity( if peer in ('me', 'self'): return _tl.InputPeerSelf() - # No InputPeer, cached peer, or known string. Fetch from disk cache + # No InputPeer, cached peer, or known string. Fetch from session cache try: - return self.session.get_input_entity(peer) + peer = utils.get_peer(peer) + if isinstance(peer, _tl.PeerUser): + entity = await self.session.get_entity(Entity.USER, peer.user_id) + if entity: + return _tl.InputPeerUser(entity.id, entity.access_hash) + elif isinstance(peer, _tl.PeerChat): + return _tl.InputPeerChat(peer.chat_id) + elif isinstance(peer, _tl.PeerChannel): + entity = await self.session.get_entity(Entity.CHANNEL, peer.user_id) + if entity: + return _tl.InputPeerChannel(entity.id, entity.access_hash) except ValueError: pass @@ -387,12 +400,6 @@ async def _get_entity_from_string(self: 'TelegramClient', string): return next(x for x in result.chats if x.id == pid) except StopIteration: pass - try: - # Nobody with this username, maybe it's an exact name/title - return await self.get_entity( - self.session.get_input_entity(string)) - except ValueError: - pass raise ValueError( 'Cannot find any entity corresponding to "{}"'.format(string) diff --git a/telethon/_misc/entitycache.py b/telethon/_misc/entitycache.py index a191dc6f..685aa411 100644 --- a/telethon/_misc/entitycache.py +++ b/telethon/_misc/entitycache.py @@ -3,6 +3,7 @@ import itertools from .._misc import utils from .. import _tl +from ..sessions.types import Entity # Which updates have the following fields? _has_field = { @@ -51,27 +52,60 @@ class EntityCache: """ In-memory input entity cache, defaultdict-like behaviour. """ - def add(self, entities): + def add(self, entities, _mappings={ + _tl.User.CONSTRUCTOR_ID: lambda e: (Entity.BOT if e.bot else Entity.USER, e.id, e.access_hash), + _tl.UserFull.CONSTRUCTOR_ID: lambda e: (Entity.BOT if e.user.bot else Entity.USER, e.user.id, e.user.access_hash), + _tl.Chat.CONSTRUCTOR_ID: lambda e: (Entity.GROUP, e.id, 0), + _tl.ChatFull.CONSTRUCTOR_ID: lambda e: (Entity.GROUP, e.id, 0), + _tl.ChatEmpty.CONSTRUCTOR_ID: lambda e: (Entity.GROUP, e.id, 0), + _tl.ChatForbidden.CONSTRUCTOR_ID: lambda e: (Entity.GROUP, e.id, 0), + _tl.Channel.CONSTRUCTOR_ID: lambda e: ( + Entity.MEGAGROUP if e.megagroup else (Entity.GIGAGROUP if e.gigagroup else Entity.CHANNEL), + e.id, + e.access_hash, + ), + _tl.ChannelForbidden.CONSTRUCTOR_ID: lambda e: (Entity.MEGAGROUP if e.megagroup else Entity.CHANNEL, e.id, e.access_hash), + }): """ Adds the given entities to the cache, if they weren't saved before. + + Returns a list of Entity that can be saved in the session. """ if not utils.is_list_like(entities): # Invariant: all "chats" and "users" are always iterables, - # and "user" never is (so we wrap it inside a list). + # and "user" and "chat" never are (so we wrap them inside a list). + # + # Itself may be already the entity we want to cache. entities = itertools.chain( + [entities], getattr(entities, 'chats', []), getattr(entities, 'users', []), - (hasattr(entities, 'user') and [entities.user]) or [] + (hasattr(entities, 'user') and [entities.user]) or [], + (hasattr(entities, 'chat') and [entities.user]) or [], ) - for entity in entities: + rows = [] + for e in entities: try: - pid = utils.get_peer_id(entity) - if pid not in self.__dict__: - # Note: `get_input_peer` already checks for `access_hash` - self.__dict__[pid] = utils.get_input_peer(entity) - except TypeError: - pass + mapper = _mappings[e.CONSTRUCTOR_ID] + except (AttributeError, KeyError): + continue + + ty, id, access_hash = mapper(e) + + # Need to check for non-zero access hash unless it's a group (#354 and #392). + # Also check it's not `min` (`access_hash` usage is limited since layer 102). + if not getattr(e, 'min', False) and (access_hash or ty == Entity.GROUP): + rows.append(Entity(ty, id, access_hash)) + if id not in self.__dict__: + if ty in (Entity.USER, Entity.BOT): + self.__dict__[id] = _tl.InputPeerUser(id, access_hash) + elif ty in (Entity.GROUP): + self.__dict__[id] = _tl.InputPeerChat(id) + elif ty in (Entity.CHANNEL, Entity.MEGAGROUP, Entity.GIGAGROUP): + self.__dict__[id] = _tl.InputPeerChannel(id, access_hash) + + return rows def __getitem__(self, item): """ diff --git a/telethon/_network/mtprotosender.py b/telethon/_network/mtprotosender.py index 9a873fc8..16bd6e32 100644 --- a/telethon/_network/mtprotosender.py +++ b/telethon/_network/mtprotosender.py @@ -34,9 +34,8 @@ class MTProtoSender: A new authorization key will be generated on connection if no other key exists yet. """ - def __init__(self, auth_key, *, loggers, + def __init__(self, *, loggers, retries=5, delay=1, auto_reconnect=True, connect_timeout=None, - auth_key_callback=None, update_callback=None, auto_reconnect_callback=None): self._connection = None self._loggers = loggers @@ -45,7 +44,6 @@ class MTProtoSender: self._delay = delay self._auto_reconnect = auto_reconnect self._connect_timeout = connect_timeout - self._auth_key_callback = auth_key_callback self._update_callback = update_callback self._auto_reconnect_callback = auto_reconnect_callback self._connect_lock = asyncio.Lock() @@ -67,7 +65,7 @@ class MTProtoSender: self._recv_loop_handle = None # Preserving the references of the AuthKey and state is important - self.auth_key = auth_key or AuthKey(None) + self.auth_key = AuthKey(None) self._state = MTProtoState(self.auth_key, loggers=self._loggers) # Outgoing messages are put in a queue and sent in a batch. @@ -283,13 +281,6 @@ class MTProtoSender: self.auth_key.key, self._state.time_offset = \ await authenticator.do_authentication(plain) - # This is *EXTREMELY* important since we don't control - # external references to the authorization key, we must - # notify whenever we change it. This is crucial when we - # switch to different data centers. - if self._auth_key_callback: - self._auth_key_callback(self.auth_key) - self._log.debug('auth_key generation success!') return True except (SecurityError, AssertionError) as e: @@ -372,8 +363,6 @@ class MTProtoSender: if isinstance(e, InvalidBufferError) and e.code == 404: self._log.info('Broken authorization key; resetting') self.auth_key.key = None - if self._auth_key_callback: - self._auth_key_callback(None) ok = False break @@ -516,8 +505,6 @@ class MTProtoSender: if isinstance(e, InvalidBufferError) and e.code == 404: self._log.info('Broken authorization key; resetting') self.auth_key.key = None - if self._auth_key_callback: - self._auth_key_callback(None) await self._disconnect(error=e) else: From d33402f02e344b154ca00b3748c34f6d2054e32f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 19 Sep 2021 17:08:34 +0200 Subject: [PATCH 080/256] Fix _update_loop could get stuck in an infinite loop with no feedback --- telethon/_client/updates.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index 0ae8b299..2e7483ac 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -173,16 +173,17 @@ async def _update_loop(self: 'TelegramClient'): rnd = lambda: random.randrange(-2**63, 2**63) while self.is_connected(): try: - await asyncio.wait_for( - self.disconnected, timeout=60 - ) + await asyncio.wait_for(self.run_until_disconnected(), timeout=60) continue # We actually just want to act upon timeout except asyncio.TimeoutError: pass except asyncio.CancelledError: return - except Exception: - continue # Any disconnected exception should be ignored + except Exception as e: + # Any disconnected exception should be ignored (or it may hint at + # another problem, leading to an infinite loop, hence the logging call) + self._log[__name__].info('Exception waiting on a disconnect: %s', e) + continue # Check if we have any exported senders to clean-up periodically await self._clean_exported_senders() From 9479e215fbada441466640d64fd39951579eef17 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 19 Sep 2021 17:08:51 +0200 Subject: [PATCH 081/256] Fix remaining upgraded uses of the session to work correctly --- telethon/_client/telegrambaseclient.py | 7 +++++-- telethon/_client/users.py | 23 +++++++++++------------ telethon/sessions/abstract.py | 4 +++- telethon/sessions/memory.py | 2 +- telethon/sessions/sqlite.py | 15 ++++++++------- 5 files changed, 28 insertions(+), 23 deletions(-) diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 1050c2a5..1add1abb 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -319,6 +319,9 @@ async def connect(self: 'TelegramClient') -> None: # TODO Get state from channels too self._state_cache = statecache.StateCache(state, self._log) + # Use known key, if any + self._sender.auth_key.key = dc.auth + if not await self._sender.connect(self._connection( str(ipaddress.ip_address(dc.ipv6 or dc.ipv4)), dc.port, @@ -330,8 +333,8 @@ async def connect(self: 'TelegramClient') -> None: # We don't want to init or modify anything if we were already connected return - if self._sender.auth_key.key != dc.key: - dc.key = self._sender.auth_key.key + if self._sender.auth_key.key != dc.auth: + dc.auth = self._sender.auth_key.key await self.session.insert_dc(dc) await self.session.save() diff --git a/telethon/_client/users.py b/telethon/_client/users.py index 394baee9..18a6f0f4 100644 --- a/telethon/_client/users.py +++ b/telethon/_client/users.py @@ -271,19 +271,18 @@ async def get_input_entity( # No InputPeer, cached peer, or known string. Fetch from session cache try: - peer = utils.get_peer(peer) - if isinstance(peer, _tl.PeerUser): - entity = await self.session.get_entity(Entity.USER, peer.user_id) - if entity: - return _tl.InputPeerUser(entity.id, entity.access_hash) - elif isinstance(peer, _tl.PeerChat): - return _tl.InputPeerChat(peer.chat_id) - elif isinstance(peer, _tl.PeerChannel): - entity = await self.session.get_entity(Entity.CHANNEL, peer.user_id) - if entity: - return _tl.InputPeerChannel(entity.id, entity.access_hash) - except ValueError: + peer_id = utils.get_peer_id(peer) + except TypeError: pass + else: + entity = await self.session.get_entity(None, peer_id) + if entity: + if entity.ty in (Entity.USER, Entity.BOT): + return _tl.InputPeerUser(entity.id, entity.access_hash) + elif entity.ty in (Entity.GROUP): + return _tl.InputPeerChat(peer.chat_id) + elif entity.ty in (Entity.CHANNEL, Entity.MEGAGROUP, Entity.GIGAGROUP): + return _tl.InputPeerChannel(entity.id, entity.access_hash) # Only network left to try if isinstance(peer, str): diff --git a/telethon/sessions/abstract.py b/telethon/sessions/abstract.py index 4cdc9131..2b28ae76 100644 --- a/telethon/sessions/abstract.py +++ b/telethon/sessions/abstract.py @@ -59,7 +59,7 @@ class Session(ABC): raise NotImplementedError @abstractmethod - async def get_entity(self, ty: int, id: int) -> Optional[Entity]: + async def get_entity(self, ty: Optional[int], id: int) -> Optional[Entity]: """ Get the `Entity` with matching ``ty`` and ``id``. @@ -75,6 +75,8 @@ class Session(ABC): the corresponding ``access_hash`` should still be returned. You may use `types.canonical_entity_type` to find out the canonical type. + + A ``ty`` with the value of ``None`` should be treated as "any entity with matching ID". """ raise NotImplementedError diff --git a/telethon/sessions/memory.py b/telethon/sessions/memory.py index 67602ec9..1c86aff7 100644 --- a/telethon/sessions/memory.py +++ b/telethon/sessions/memory.py @@ -36,7 +36,7 @@ class MemorySession(Session): async def insert_entities(self, entities: List[Entity]): self.entities.update((e.id, (e.ty, e.access_hash)) for e in entities) - async def get_entity(self, ty: int, id: int) -> Optional[Entity]: + async def get_entity(self, ty: Optional[int], id: int) -> Optional[Entity]: try: ty, access_hash = self.entities[id] return Entity(ty, id, access_hash) diff --git a/telethon/sessions/sqlite.py b/telethon/sessions/sqlite.py index 5cd288aa..5bfc0433 100644 --- a/telethon/sessions/sqlite.py +++ b/telethon/sessions/sqlite.py @@ -55,13 +55,17 @@ class SQLiteSession(Session): self._upgrade_database(old=version) c.execute("delete from version") c.execute("insert into version values (?)", (CURRENT_VERSION,)) - self.save() + self._conn.commit() else: # Tables don't exist, create new ones + self._create_table(c, 'version (version integer primary key)') self._mk_tables(c) c.execute("insert into version values (?)", (CURRENT_VERSION,)) - c.close() - self.save() + self._conn.commit() + + # Must have committed or else the version will not have been updated while new tables + # exist, leading to a half-upgraded state. + c.close() def _upgrade_database(self, old): c = self._cursor() @@ -146,9 +150,6 @@ class SQLiteSession(Session): def _mk_tables(self, c): self._create_table( c, - '''version ( - version integer primary key - )''', '''datacenter ( id integer primary key, ip text not null, @@ -243,7 +244,7 @@ class SQLiteSession(Session): finally: c.close() - async def get_entity(self, ty: int, id: int) -> Optional[Entity]: + async def get_entity(self, ty: Optional[int], id: int) -> Optional[Entity]: row = self._execute('select ty, id, access_hash from entity where id = ?', id) return Entity(*row) if row else None From d60ebbe6eaa2b5bf74a7d13bc7e86c961fe8d889 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 19 Sep 2021 17:21:11 +0200 Subject: [PATCH 082/256] Fix _get_peer was relying on old utils.resolve_id --- telethon/_client/messages.py | 12 ++++++++++-- telethon/_client/telegramclient.py | 3 --- telethon/_client/users.py | 4 ---- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/telethon/_client/messages.py b/telethon/_client/messages.py index 69f00a0c..8305f60c 100644 --- a/telethon/_client/messages.py +++ b/telethon/_client/messages.py @@ -293,7 +293,7 @@ class _IDsIter(requestiter.RequestIter): else: r = await self.client(_tl.fn.messages.GetMessages(ids)) if self._entity: - from_id = await self.client._get_peer(self._entity) + from_id = await _get_peer(self.client, self._entity) if isinstance(r, _tl.messages.MessagesNotModified): self.buffer.extend(None for _ in ids) @@ -318,6 +318,14 @@ class _IDsIter(requestiter.RequestIter): self.buffer.append(_custom.Message._new(self.client, message, entities, self._entity)) +async def _get_peer(self: 'TelegramClient', input_peer: 'hints.EntityLike'): + try: + return utils.get_peer(input_peer) + except TypeError: + # Can only be self by now + return _tl.PeerUser(await self.get_peer_id(input_peer)) + + def get_messages( self: 'TelegramClient', entity: 'hints.EntityLike', @@ -480,7 +488,7 @@ async def send_message( if isinstance(result, _tl.UpdateShortSentMessage): return _custom.Message._new(self, _tl.Message( id=result.id, - peer_id=await self._get_peer(entity), + peer_id=await _get_peer(self, entity), message=message, date=result.date, out=result.out, diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index e5dc6f8f..62511cd8 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -3573,9 +3573,6 @@ class TelegramClient: ttl=None): return await uploads._file_to_media(**locals()) - async def _get_peer(self: 'TelegramClient', peer: 'hints.EntityLike'): - return await users._get_peer(**locals()) - def _get_response_message(self: 'TelegramClient', request, result, input_chat): return messageparse._get_response_message(**locals()) diff --git a/telethon/_client/users.py b/telethon/_client/users.py index 18a6f0f4..5f3d7116 100644 --- a/telethon/_client/users.py +++ b/telethon/_client/users.py @@ -323,10 +323,6 @@ async def get_input_entity( .format(peer, type(peer).__name__) ) -async def _get_peer(self: 'TelegramClient', peer: 'hints.EntityLike'): - i, cls = utils.resolve_id(await self.get_peer_id(peer)) - return cls(i) - async def get_peer_id( self: 'TelegramClient', peer: 'hints.EntityLike') -> int: From 58c0a5bc24a8f7e047c020620ccafdadf8bbb924 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 19 Sep 2021 17:30:31 +0200 Subject: [PATCH 083/256] Make IPv4 mandatory in session files --- telethon/_client/telegrambaseclient.py | 2 +- telethon/sessions/sqlite.py | 19 ++++++++++--------- telethon/sessions/types.py | 2 +- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 1add1abb..eb8b6cd3 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -323,7 +323,7 @@ async def connect(self: 'TelegramClient') -> None: self._sender.auth_key.key = dc.auth if not await self._sender.connect(self._connection( - str(ipaddress.ip_address(dc.ipv6 or dc.ipv4)), + str(ipaddress.ip_address((self._use_ipv6 and dc.ipv6) or dc.ipv4)), dc.port, dc.id, loggers=self._log, diff --git a/telethon/sessions/sqlite.py b/telethon/sessions/sqlite.py index 5bfc0433..00cc3895 100644 --- a/telethon/sessions/sqlite.py +++ b/telethon/sessions/sqlite.py @@ -107,8 +107,8 @@ class SQLiteSession(Session): if old == 7: self._mk_tables(c) c.execute(''' - insert into datacenter (id, ip, port, auth) - select dc_id, server_address, port, auth_key + insert into datacenter (id, ipv4, ipv6, port, auth) + select dc_id, server_address, server_address, port, auth_key from sessions ''') c.execute(''' @@ -152,7 +152,8 @@ class SQLiteSession(Session): c, '''datacenter ( id integer primary key, - ip text not null, + ipv4 text not null, + ipv6 text, port integer not null, auth blob not null )''', @@ -179,9 +180,10 @@ class SQLiteSession(Session): async def insert_dc(self, dc: DataCenter): self._execute( - 'insert or replace into datacenter values (?,?,?,?)', + 'insert or replace into datacenter values (?,?,?,?,?)', dc.id, - str(ipaddress.ip_address(dc.ipv6 or dc.ipv4)), + str(ipaddress.ip_address(dc.ipv4)), + str(ipaddress.ip_address(dc.ipv6)) if dc.ipv6 else None, dc.port, dc.auth ) @@ -189,12 +191,11 @@ class SQLiteSession(Session): async def get_all_dc(self) -> List[DataCenter]: c = self._cursor() res = [] - for (id, ip, port, auth) in c.execute('select * from datacenter'): - ip = ipaddress.ip_address(ip) + for (id, ipv4, ipv6, port, auth) in c.execute('select * from datacenter'): res.append(DataCenter( id=id, - ipv4=int(ip) if ip.version == 4 else None, - ipv6=int(ip) if ip.version == 6 else None, + ipv4=int(ipaddress.ip_address(ipv4)), + ipv6=int(ipaddress.ip_address(ipv6)) if ipv6 else None, port=port, auth=auth, )) diff --git a/telethon/sessions/types.py b/telethon/sessions/types.py index 39033a1f..5fb0d608 100644 --- a/telethon/sessions/types.py +++ b/telethon/sessions/types.py @@ -15,7 +15,7 @@ class DataCenter: def __init__( self, id: int, - ipv4: Optional[int], + ipv4: int, ipv6: Optional[int], port: int, auth: bytes From 93dd2a186a63f45a079e008c9c09d4a8606072ec Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 19 Sep 2021 17:41:40 +0200 Subject: [PATCH 084/256] Refresh DC info on connection --- telethon/_client/telegrambaseclient.py | 39 ++++++++++++++++++-------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index eb8b6cd3..5afd0328 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -296,16 +296,10 @@ def set_flood_sleep_threshold(self, value): async def connect(self: 'TelegramClient') -> None: - all_dc = await self.session.get_all_dc() + all_dc = {dc.id: dc for dc in await self.session.get_all_dc()} state = await self.session.get_state() - dc = None - if state: - for d in all_dc: - if d.id == state.dc_id: - dc = d - break - + dc = all_dc.get(state.dc_id) if state else None if dc is None: dc = DataCenter( id=DEFAULT_DC_ID, @@ -314,6 +308,7 @@ async def connect(self: 'TelegramClient') -> None: port=DEFAULT_PORT, auth=b'', ) + all_dc[dc.id] = dc # Update state (for catching up after a disconnection) # TODO Get state from channels too @@ -335,15 +330,37 @@ async def connect(self: 'TelegramClient') -> None: if self._sender.auth_key.key != dc.auth: dc.auth = self._sender.auth_key.key - await self.session.insert_dc(dc) - await self.session.save() + # Need to send invokeWithLayer for things to work out. + # Make the most out of this opportunity by also refreshing our state. + # During the v1 to v2 migration, this also correctly sets the IPv* columns. self._init_request.query = _tl.fn.help.GetConfig() - await self._sender.send(_tl.fn.InvokeWithLayer( + config = await self._sender.send(_tl.fn.InvokeWithLayer( _tl.LAYER, self._init_request )) + for dc in config.dc_options: + if dc.media_only or dc.tcpo_only or dc.cdn: + continue + + ip = int(ipaddress.ip_address(dc.ip_address)) + if dc.id in all_dc: + all_dc[dc.id].port = dc.port + if dc.ipv6: + all_dc[dc.id].ipv6 = ip + else: + all_dc[dc.id].ipv4 = ip + elif dc.ipv6: + all_dc[dc.id] = DataCenter(dc.id, None, ip, dc.port, b'') + else: + all_dc[dc.id] = DataCenter(dc.id, ip, None, dc.port, b'') + + for dc in all_dc.values(): + await self.session.insert_dc(dc) + + await self.session.save() + self._updates_handle = self.loop.create_task(self._update_loop()) def is_connected(self: 'TelegramClient') -> bool: From 545e9d69ce3734a1285b84fabed6a136f7e3215f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 19 Sep 2021 17:51:05 +0200 Subject: [PATCH 085/256] Cache session_state and all_dcs right after connect --- telethon/_client/downloads.py | 5 +- telethon/_client/messages.py | 5 +- telethon/_client/telegrambaseclient.py | 121 ++++++++----------------- telethon/_client/telegramclient.py | 3 - telethon/_misc/statecache.py | 2 +- 5 files changed, 39 insertions(+), 97 deletions(-) diff --git a/telethon/_client/downloads.py b/telethon/_client/downloads.py index 99932cdc..50a383dd 100644 --- a/telethon/_client/downloads.py +++ b/telethon/_client/downloads.py @@ -39,10 +39,7 @@ class _DirectDownloadIter(requestiter.RequestIter): self._msg_data = msg_data self._timed_out = False - # TODO should cache current session state - state = await self.client.session.get_state() - - self._exported = dc_id and state.dc_id != dc_id + self._exported = dc_id and self.client._session_state.dc_id != dc_id if not self._exported: # The used sender will also change if ``FileMigrateError`` occurs self._sender = self.client._sender diff --git a/telethon/_client/messages.py b/telethon/_client/messages.py index 8305f60c..5b7ca110 100644 --- a/telethon/_client/messages.py +++ b/telethon/_client/messages.py @@ -597,10 +597,7 @@ async def edit_message( ) # Invoke `messages.editInlineBotMessage` from the right datacenter. # Otherwise, Telegram will error with `MESSAGE_ID_INVALID` and do nothing. - # TODO should cache current session state - state = await self.session.get_state() - - exported = state.dc_id != entity.dc_id + exported = self._session_state.dc_id != entity.dc_id if exported: try: sender = await self._borrow_exported_sender(entity.dc_id) diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 5afd0328..960b074f 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -142,6 +142,11 @@ def init( # TODO Session should probably return all cached # info of entities, not just the input versions self.session = session + + # Cache session data for convenient access + self._session_state = None + self._all_dcs = None + self._entity_cache = entitycache.EntityCache() self.api_id = int(api_id) self.api_hash = api_hash @@ -296,10 +301,19 @@ def set_flood_sleep_threshold(self, value): async def connect(self: 'TelegramClient') -> None: - all_dc = {dc.id: dc for dc in await self.session.get_all_dc()} - state = await self.session.get_state() + self._all_dcs = {dc.id: dc for dc in await self.session.get_all_dc()} + self._session_state = await self.session.get_state() or SessionState( + user_id=0, + dc_id=DEFAULT_DC_ID, + bot=False, + pts=0, + qts=0, + date=0, + seq=0, + takeout_id=None, + ) - dc = all_dc.get(state.dc_id) if state else None + dc = self._all_dcs.get(self._session_state.dc_id) if dc is None: dc = DataCenter( id=DEFAULT_DC_ID, @@ -308,11 +322,11 @@ async def connect(self: 'TelegramClient') -> None: port=DEFAULT_PORT, auth=b'', ) - all_dc[dc.id] = dc + self._all_dcs[dc.id] = dc # Update state (for catching up after a disconnection) # TODO Get state from channels too - self._state_cache = statecache.StateCache(state, self._log) + self._state_cache = statecache.StateCache(self._session_state, self._log) # Use known key, if any self._sender.auth_key.key = dc.auth @@ -345,18 +359,18 @@ async def connect(self: 'TelegramClient') -> None: continue ip = int(ipaddress.ip_address(dc.ip_address)) - if dc.id in all_dc: - all_dc[dc.id].port = dc.port + if dc.id in self._all_dcs: + self._all_dcs[dc.id].port = dc.port if dc.ipv6: - all_dc[dc.id].ipv6 = ip + self._all_dcs[dc.id].ipv6 = ip else: - all_dc[dc.id].ipv4 = ip + self._all_dcs[dc.id].ipv4 = ip elif dc.ipv6: - all_dc[dc.id] = DataCenter(dc.id, None, ip, dc.port, b'') + self._all_dcs[dc.id] = DataCenter(dc.id, None, ip, dc.port, b'') else: - all_dc[dc.id] = DataCenter(dc.id, ip, None, dc.port, b'') + self._all_dcs[dc.id] = DataCenter(dc.id, ip, None, dc.port, b'') - for dc in all_dc.values(): + for dc in self._all_dcs.values(): await self.session.insert_dc(dc) await self.session.save() @@ -419,11 +433,10 @@ async def _disconnect_coro(self: 'TelegramClient'): pts, date = self._state_cache[None] if pts and date: - state = await self.session.get_state() - if state: - state.pts = pts - state.date = date - await self.session.set_state(state) + if self._session_state: + self._session_state.pts = pts + self._session_state.date = date + await self.session.set_state(self._session_state) await self.session.save() async def _disconnect(self: 'TelegramClient'): @@ -442,76 +455,14 @@ async def _switch_dc(self: 'TelegramClient', new_dc): Permanently switches the current connection to the new data center. """ self._log[__name__].info('Reconnecting to new data center %s', new_dc) - dc = await _refresh_and_get_dc(self, new_dc) - state = await self.session.get_state() - if state is None: - state = SessionState( - user_id=0, - dc_id=dc.id, - bot=False, - pts=0, - qts=0, - date=0, - seq=0, - takeout_id=None, - ) - else: - state.dc_id = dc.id - - await self.session.set_state(dc.id) + self._session_state.dc_id = new_dc + await self.session.set_state(self._session_state) await self.session.save() await _disconnect(self) return await self.connect() - -async def _refresh_and_get_dc(self: 'TelegramClient', dc_id): - """ - Gets the Data Center (DC) associated to `dc_id`. - - Also take this opportunity to refresh the addresses stored in the session if needed. - """ - cls = self.__class__ - if not cls._config: - cls._config = await self(_tl.fn.help.GetConfig()) - all_dc = {dc.id: dc for dc in await self.session.get_all_dc()} - for dc in cls._config.dc_options: - if dc.media_only or dc.tcpo_only or dc.cdn: - continue - - ip = int(ipaddress.ip_address(dc.ip_address)) - if dc.id in all_dc: - all_dc[dc.id].port = dc.port - if dc.ipv6: - all_dc[dc.id].ipv6 = ip - else: - all_dc[dc.id].ipv4 = ip - elif dc.ipv6: - all_dc[dc.id] = DataCenter(dc.id, None, ip, dc.port, b'') - else: - all_dc[dc.id] = DataCenter(dc.id, ip, None, dc.port, b'') - - for dc in all_dc.values(): - await self.session.insert_dc(dc) - await self.session.save() - - try: - return next( - dc for dc in cls._config.dc_options - if dc.id == dc_id - and bool(dc.ipv6) == self._use_ipv6 and not dc.cdn - ) - except StopIteration: - self._log[__name__].warning( - 'Failed to get DC %swith use_ipv6 = %s; retrying ignoring IPv6 check', - dc_id, self._use_ipv6 - ) - return next( - dc for dc in cls._config.dc_options - if dc.id == dc_id and not dc.cdn - ) - async def _create_exported_sender(self: 'TelegramClient', dc_id): """ Creates a new exported `MTProtoSender` for the given `dc_id` and @@ -519,14 +470,14 @@ async def _create_exported_sender(self: 'TelegramClient', dc_id): """ # Thanks badoualy/kotlogram on /telegram/api/DefaultTelegramClient.kt # for clearly showing how to export the authorization - dc = await _refresh_and_get_dc(self, dc_id) + dc = self._all_dcs[dc_id] # Can't reuse self._sender._connection as it has its own seqno. # # If one were to do that, Telegram would reset the connection # with no further clues. sender = MTProtoSender(loggers=self._log) await sender.connect(self._connection( - dc.ip_address, + str(ipaddress.ip_address((self._use_ipv6 and dc.ipv6) or dc.ipv4)), dc.port, dc.id, loggers=self._log, @@ -559,9 +510,9 @@ async def _borrow_exported_sender(self: 'TelegramClient', dc_id): self._borrowed_senders[dc_id] = (state, sender) elif state.need_connect(): - dc = await _refresh_and_get_dc(self, dc_id) + dc = self._all_dcs[dc_id] await sender.connect(self._connection( - dc.ip_address, + str(ipaddress.ip_address((self._use_ipv6 and dc.ipv6) or dc.ipv4)), dc.port, dc.id, loggers=self._log, diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 62511cd8..10db3557 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -2704,9 +2704,6 @@ class TelegramClient: # Current TelegramClient version __version__ = version.__version__ - # Cached server configuration (with .dc_options), can be "global" - _config = None - def __init__( self: 'TelegramClient', session: 'typing.Union[str, Session]', diff --git a/telethon/_misc/statecache.py b/telethon/_misc/statecache.py index 7f3ddf59..c1a6d7c9 100644 --- a/telethon/_misc/statecache.py +++ b/telethon/_misc/statecache.py @@ -36,7 +36,7 @@ class StateCache: # each update in case they need to fetch missing entities. self._logger = loggers[__name__] if initial: - self._pts_date = initial.pts, initial.date + self._pts_date = initial.pts or None, initial.date or None else: self._pts_date = None, None From 35a6d1e294f9e7953bcee85f7e256663bd99b242 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 19 Sep 2021 17:59:35 +0200 Subject: [PATCH 086/256] Fix SessionState did not store takeout_id --- telethon/sessions/types.py | 1 + 1 file changed, 1 insertion(+) diff --git a/telethon/sessions/types.py b/telethon/sessions/types.py index 5fb0d608..51d4ecb5 100644 --- a/telethon/sessions/types.py +++ b/telethon/sessions/types.py @@ -63,6 +63,7 @@ class SessionState: self.qts = qts self.date = date self.seq = seq + self.takeout_id = takeout_id class ChannelState: From 016347474a3bc700e1708894a5d0151a0ae0efc5 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 19 Sep 2021 18:01:01 +0200 Subject: [PATCH 087/256] Populate current user on connection if it's not yet saved --- telethon/_client/telegrambaseclient.py | 43 ++++++++++++++++++++------ 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 960b074f..a447ae61 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -302,16 +302,22 @@ def set_flood_sleep_threshold(self, value): async def connect(self: 'TelegramClient') -> None: self._all_dcs = {dc.id: dc for dc in await self.session.get_all_dc()} - self._session_state = await self.session.get_state() or SessionState( - user_id=0, - dc_id=DEFAULT_DC_ID, - bot=False, - pts=0, - qts=0, - date=0, - seq=0, - takeout_id=None, - ) + self._session_state = await self.session.get_state() + + if self._session_state is None: + try_fetch_user = False + self._session_state = SessionState( + user_id=0, + dc_id=DEFAULT_DC_ID, + bot=False, + pts=0, + qts=0, + date=0, + seq=0, + takeout_id=None, + ) + else: + try_fetch_user = self._session_state.user_id == 0 dc = self._all_dcs.get(self._session_state.dc_id) if dc is None: @@ -373,6 +379,23 @@ async def connect(self: 'TelegramClient') -> None: for dc in self._all_dcs.values(): await self.session.insert_dc(dc) + if try_fetch_user: + # If there was a previous session state, but the current user ID is 0, it means we've + # migrated and not yet populated the current user (or the client connected but never + # logged in). Attempt to fetch the user now. If it works, also get the update state. + me = await self.get_me() + if me: + self._session_state.user_id = me.id + self._session_state.bot = me.bot + + state = await self(_tl.fn.updates.GetState()) + self._session_state.pts = state.pts + self._session_state.qts = state.qts + self._session_state.date = int(state.date.timestamp()) + self._session_state.seq = state.seq + + await self.session.set_state(self._session_state) + await self.session.save() self._updates_handle = self.loop.create_task(self._update_loop()) From 3f13357d0f8e86772d61149d54d30469bcfcc0b1 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 19 Sep 2021 18:02:08 +0200 Subject: [PATCH 088/256] Fix SQLiteSession.set_state did not always clear old state For instance, when we stored a user_id of 0 because we did not login yet. --- telethon/sessions/sqlite.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/telethon/sessions/sqlite.py b/telethon/sessions/sqlite.py index 00cc3895..2ea419be 100644 --- a/telethon/sessions/sqlite.py +++ b/telethon/sessions/sqlite.py @@ -202,17 +202,22 @@ class SQLiteSession(Session): return res async def set_state(self, state: SessionState): - self._execute( - 'insert or replace into session values (?,?,?,?,?,?,?,?)', - state.user_id, - state.dc_id, - int(state.bot), - state.pts, - state.qts, - state.date, - state.seq, - state.takeout_id, - ) + c = self._cursor() + try: + self._execute('delete from session') + self._execute( + 'insert into session values (?,?,?,?,?,?,?,?)', + state.user_id, + state.dc_id, + int(state.bot), + state.pts, + state.qts, + state.date, + state.seq, + state.takeout_id, + ) + finally: + c.close() async def get_state(self) -> Optional[SessionState]: row = self._execute('select * from session') From cc3d4145d85ad3b0b31faba38206d0e1f4dbaab5 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 19 Sep 2021 18:15:09 +0200 Subject: [PATCH 089/256] Update and persist session state on successful login --- .gitignore | 1 + telethon/_client/auth.py | 21 ++++++++++++++++----- telethon/_client/telegrambaseclient.py | 11 +---------- telethon/_client/telegramclient.py | 4 ++-- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index 1e497d62..3856ed42 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ # User session *.session +sessions/ /usermedia/ # Builds and testing diff --git a/telethon/_client/auth.py b/telethon/_client/auth.py index 296656f7..365e7257 100644 --- a/telethon/_client/auth.py +++ b/telethon/_client/auth.py @@ -253,7 +253,7 @@ async def sign_in( self._tos = result.terms_of_service raise errors.PhoneNumberUnoccupiedError(request=request) - return _on_login(self, result.user) + return await _update_session_state(self, result.user) async def sign_up( self: 'TelegramClient', @@ -306,17 +306,28 @@ async def sign_up( await self( _tl.fn.help.AcceptTermsOfService(self._tos.id)) - return _on_login(self, result.user) + return await _update_session_state(self, result.user) -def _on_login(self, user): +async def _update_session_state(self, user, save=True): """ Callback called whenever the login or sign up process completes. Returns the input user parameter. """ - self._bot = bool(user.bot) - self._self_input_peer = utils.get_input_peer(user, allow_self=False) self._authorized = True + self._session_state.user_id = user.id + self._session_state.bot = user.bot + + state = await self(_tl.fn.updates.GetState()) + self._session_state.pts = state.pts + self._session_state.qts = state.qts + self._session_state.date = int(state.date.timestamp()) + self._session_state.seq = state.seq + + await self.session.set_state(self._session_state) + if save: + await self.session.save() + return user async def send_code_request( diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index a447ae61..38ad77ed 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -385,16 +385,7 @@ async def connect(self: 'TelegramClient') -> None: # logged in). Attempt to fetch the user now. If it works, also get the update state. me = await self.get_me() if me: - self._session_state.user_id = me.id - self._session_state.bot = me.bot - - state = await self(_tl.fn.updates.GetState()) - self._session_state.pts = state.pts - self._session_state.qts = state.qts - self._session_state.date = int(state.date.timestamp()) - self._session_state.seq = state.seq - - await self.session.set_state(self._session_state) + await self._update_session_state(me, save=False) await self.session.save() diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 10db3557..15a7db75 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -3598,8 +3598,8 @@ class TelegramClient: async def _handle_auto_reconnect(self: 'TelegramClient'): return await updates._handle_auto_reconnect(**locals()) - def _self_id(self: 'TelegramClient') -> typing.Optional[int]: - return users._self_id(**locals()) + async def _update_session_state(self, user, save=True): + return await auth._update_session_state(**locals()) # endregion Private From 3b1660669e6eecd789b16de27596a33f7bc73518 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 19 Sep 2021 18:16:12 +0200 Subject: [PATCH 090/256] Remove self input user and bot cache from client The session_state cache can be used instead. This does put get_me with input_peer at a disadvantage, but I expect this is not used all that often, since 'me' does just fine. --- telethon/_client/auth.py | 2 -- telethon/_client/telegrambaseclient.py | 4 ---- telethon/_client/updates.py | 14 +------------ telethon/_client/users.py | 29 +++----------------------- telethon/types/_custom/message.py | 4 ++-- 5 files changed, 6 insertions(+), 47 deletions(-) diff --git a/telethon/_client/auth.py b/telethon/_client/auth.py index 365e7257..031b3a11 100644 --- a/telethon/_client/auth.py +++ b/telethon/_client/auth.py @@ -377,8 +377,6 @@ async def log_out(self: 'TelegramClient') -> bool: except errors.RPCError: return False - self._bot = None - self._self_input_peer = None self._authorized = False self._state_cache.reset() diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 38ad77ed..8bbf6051 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -281,10 +281,6 @@ def init( self._phone = None self._tos = None - # Sometimes we need to know who we are, cache the self peer - self._self_input_peer = None - self._bot = None - # A place to store if channels are a megagroup or not (see `edit_admin`) self._megagroup_cache = {} diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index 2e7483ac..2a03a0b3 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -255,18 +255,6 @@ async def _dispatch_update(self: 'TelegramClient', update, others, channel_id, p # ValueError("Request was unsuccessful N time(s)") for whatever reasons. pass - if not self._self_input_peer: - # Some updates require our own ID, so we must make sure - # that the event builder has offline access to it. Calling - # `get_me()` will cache it under `self._self_input_peer`. - # - # It will return `None` if we haven't logged in yet which is - # fine, we will just retry next time anyway. - try: - await self.get_me(input_peer=True) - except OSError: - pass # might not have connection - built = EventBuilderDict(self, update, others) for builder, callback in self._event_builders: @@ -452,7 +440,7 @@ class EventBuilderDict: return self.__dict__[builder] except KeyError: event = self.__dict__[builder] = builder.build( - self.update, self.others, self.client._self_id) + self.update, self.others, self.client._session_state.user_id) if isinstance(event, EventCommon): event.original_update = self.update diff --git a/telethon/_client/users.py b/telethon/_client/users.py index 5f3d7116..b653af94 100644 --- a/telethon/_client/users.py +++ b/telethon/_client/users.py @@ -135,37 +135,14 @@ async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sl async def get_me(self: 'TelegramClient', input_peer: bool = False) \ -> 'typing.Union[_tl.User, _tl.InputPeerUser]': - if input_peer and self._self_input_peer: - return self._self_input_peer - try: - me = (await self( - _tl.fn.users.GetUsers([_tl.InputUserSelf()])))[0] - - self._bot = me.bot - if not self._self_input_peer: - self._self_input_peer = utils.get_input_peer( - me, allow_self=False - ) - - return self._self_input_peer if input_peer else me + me = (await self(_tl.fn.users.GetUsers([_tl.InputUserSelf()])))[0] + return utils.get_input_peer(me, allow_self=False) if input_peer else me except errors.UnauthorizedError: return None -def _self_id(self: 'TelegramClient') -> typing.Optional[int]: - """ - Returns the ID of the logged-in user, if known. - - This property is used in every update, and some like `updateLoginToken` - occur prior to login, so it gracefully handles when no ID is known yet. - """ - return self._self_input_peer.user_id if self._self_input_peer else None - async def is_bot(self: 'TelegramClient') -> bool: - if self._bot is None: - self._bot = (await self.get_me()).bot - - return self._bot + return self._session_state.bot if self._session_state else False async def is_user_authorized(self: 'TelegramClient') -> bool: if self._authorized is None: diff --git a/telethon/types/_custom/message.py b/telethon/types/_custom/message.py index 2d8f3fbe..ae0b395e 100644 --- a/telethon/types/_custom/message.py +++ b/telethon/types/_custom/message.py @@ -255,7 +255,7 @@ class Message(ChatGetter, SenderGetter): # Make messages sent to ourselves outgoing unless they're forwarded. # This makes it consistent with official client's appearance. - if self.peer_id == _tl.PeerUser(client._self_id) and not self.fwd_from: + if self.peer_id == _tl.PeerUser(client._session_state.user_id) and not self.fwd_from: self.out = True cache = client._entity_cache @@ -644,7 +644,7 @@ class Message(ChatGetter, SenderGetter): # If the client wasn't set we can't emulate the behaviour correctly, # so as a best-effort simply return the chat peer. if self._client and not self.out and self.is_private: - return _tl.PeerUser(self._client._self_id) + return _tl.PeerUser(self._client._session_state.user_id) return self.peer_id From 26f6c62ce4c44bcdc5ad4f02f20d66d6f4924932 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 19 Sep 2021 18:17:37 +0200 Subject: [PATCH 091/256] Init update state cache to empty in init --- telethon/_client/telegrambaseclient.py | 1 + 1 file changed, 1 insertion(+) diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 8bbf6051..06f6bac8 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -146,6 +146,7 @@ def init( # Cache session data for convenient access self._session_state = None self._all_dcs = None + self._state_cache = statecache.StateCache(None, self._log) self._entity_cache = entitycache.EntityCache() self.api_id = int(api_id) From cfe47a04341ac6a6ab8984bf2236f107f9aa3ad7 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 19 Sep 2021 18:24:16 +0200 Subject: [PATCH 092/256] Correct privacy on sessions module --- telethon/{sessions => _sessions}/__init__.py | 0 telethon/{sessions => _sessions}/abstract.py | 0 telethon/{sessions => _sessions}/memory.py | 0 telethon/{sessions => _sessions}/sqlite.py | 0 telethon/{sessions => _sessions}/string.py | 0 telethon/{sessions => _sessions}/types.py | 0 telethon/sessions.py | 12 ++++++++++++ 7 files changed, 12 insertions(+) rename telethon/{sessions => _sessions}/__init__.py (100%) rename telethon/{sessions => _sessions}/abstract.py (100%) rename telethon/{sessions => _sessions}/memory.py (100%) rename telethon/{sessions => _sessions}/sqlite.py (100%) rename telethon/{sessions => _sessions}/string.py (100%) rename telethon/{sessions => _sessions}/types.py (100%) create mode 100644 telethon/sessions.py diff --git a/telethon/sessions/__init__.py b/telethon/_sessions/__init__.py similarity index 100% rename from telethon/sessions/__init__.py rename to telethon/_sessions/__init__.py diff --git a/telethon/sessions/abstract.py b/telethon/_sessions/abstract.py similarity index 100% rename from telethon/sessions/abstract.py rename to telethon/_sessions/abstract.py diff --git a/telethon/sessions/memory.py b/telethon/_sessions/memory.py similarity index 100% rename from telethon/sessions/memory.py rename to telethon/_sessions/memory.py diff --git a/telethon/sessions/sqlite.py b/telethon/_sessions/sqlite.py similarity index 100% rename from telethon/sessions/sqlite.py rename to telethon/_sessions/sqlite.py diff --git a/telethon/sessions/string.py b/telethon/_sessions/string.py similarity index 100% rename from telethon/sessions/string.py rename to telethon/_sessions/string.py diff --git a/telethon/sessions/types.py b/telethon/_sessions/types.py similarity index 100% rename from telethon/sessions/types.py rename to telethon/_sessions/types.py diff --git a/telethon/sessions.py b/telethon/sessions.py new file mode 100644 index 00000000..b4c9bf4f --- /dev/null +++ b/telethon/sessions.py @@ -0,0 +1,12 @@ +from ._sessions.types import ( + DataCenter, + SessionState, + ChannelState, + Entity, +) +from ._sessions import ( + Session, + MemorySession, + SQLiteSession, + StringSession, +) From debde6e85677391b876e253fd7121b5d612c6e7a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 24 Sep 2021 20:07:34 +0200 Subject: [PATCH 093/256] Completely overhaul errors to be generated dynamically --- .gitignore | 2 +- readthedocs/misc/v2-migration-guide.rst | 68 ++++++++++ setup.py | 2 +- telethon/_client/chats.py | 10 +- telethon/_client/dialogs.py | 2 +- telethon/_client/telegrambaseclient.py | 4 +- telethon/_client/updates.py | 7 +- telethon/_client/users.py | 34 +++-- telethon/_misc/binaryreader.py | 2 +- telethon/_misc/entitycache.py | 2 +- telethon/_network/authenticator.py | 2 +- telethon/_network/connection/connection.py | 2 +- telethon/_network/connection/tcpfull.py | 2 +- telethon/_network/mtprotoplainsender.py | 2 +- telethon/_network/mtprotosender.py | 12 +- telethon/_network/mtprotostate.py | 2 +- telethon/errors/__init__.py | 82 ++++++------ telethon/errors/{common.py => _custom.py} | 0 telethon/errors/_rpcbase.py | 144 +++++++++++++++++++++ telethon/errors/rpcbaseerrors.py | 131 ------------------- telethon/types/_custom/draft.py | 4 +- telethon/types/_custom/message.py | 2 +- telethon_generator/data/errors.csv | 35 +++-- telethon_generator/data/methods.csv | 12 +- telethon_generator/generators/errors.py | 64 ++------- telethon_generator/parsers/errors.py | 34 ++--- 26 files changed, 345 insertions(+), 318 deletions(-) rename telethon/errors/{common.py => _custom.py} (100%) create mode 100644 telethon/errors/_rpcbase.py delete mode 100644 telethon/errors/rpcbaseerrors.py diff --git a/.gitignore b/.gitignore index 3856ed42..a0864768 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ /telethon/_tl/fn/ /telethon/_tl/*.py /telethon/_tl/alltlobjects.py -/telethon/errors/rpcerrorlist.py +/telethon/errors/_generated.py # User session *.session diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index a2c32f70..b35e076d 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -140,6 +140,72 @@ for the library to work properly. If you still don't want it, you should subclas override the methods to do nothing. +Complete overhaul of errors +--------------------------- + +The following error name have changed to follow a better naming convention (clearer acronyms): + +* ``RPCError`` is now ``RpcError``. +* ``InvalidDCError`` is now ``InvalidDcError`` (lowercase ``c``). + +The base errors no longer have a ``.message`` field at the class-level. Instead, it is now an +attribute at the instance level (meaning you cannot do ``BadRequestError.message``, it must be +``bad_request_err.message`` where ``isinstance(bad_request_err, BadRequestError)``). + +The ``.message`` will gain its value at the time the error is constructed, rather than being +known beforehand. + +The parameter order for ``RpcError`` and all its subclasses are now ``(code, message, request)``, +as opposed to ``(message, request, code)``. + +Because Telegram errors can be added at any time, the library no longer generate a fixed set of +them. This means you can no longer use ``dir`` to get a full list of them. Instead, the errors +are automatically generated depending on the name you use for the error, with the following rules: + +* Numbers are removed from the name. The Telegram error ``FLOOD_WAIT_42`` is transformed into + ``FLOOD_WAIT_``. +* Underscores are removed from the name. ``FLOOD_WAIT_`` becomes ``FLOODWAIT``. +* Everything is lowercased. ``FLOODWAIT`` turns into ``floodwait``. +* While the name ends with ``error``, this suffix is removed. + +The only exception to this rule is ``2FA_CONFIRM_WAIT_0``, which is transformed as +``twofaconfirmwait`` (read as ``TwoFaConfirmWait``). + +What all this means is that, if Telegram raises a ``FLOOD_WAIT_42``, you can write the following: + +.. code-block:: python + + from telethon.errors import FloodWaitError + + try: + await client.send_message(chat, message) + except FloodWaitError as e: + print(f'Flood! wait for {e.seconds} seconds') + +Essentially, old code will keep working, but now you have the freedom to define even yet-to-be +discovered errors. This makes use of `PEP 562 `__ on +Python 3.7 and above and a more-hacky approach below (which your IDE may not love). + +Given the above rules, you could also write ``except errors.FLOOD_WAIT`` if you prefer to match +Telegram's naming conventions. We recommend Camel-Case naming with the "Error" suffix, but that's +up to you. + +All errors will include a list of ``.values`` (the extracted number) and ``.value`` (the first +number extracted, or ``None`` if ``values`` is empty). In addition to that, certain errors have +a more-recognizable alias (such as ``FloodWait`` which has ``.seconds`` for its ``.value``). + +The ``telethon.errors`` module continues to provide certain predefined ``RpcError`` to match on +the *code* of the error and not its message (for instance, match all errors with code 403 with +``ForbiddenError``). Note that a certain error message can appear with different codes too, this +is decided by Telegram. + +The ``telethon.errors`` module continues to provide custom errors used by the library such as +``TypeNotFoundError``. + +// TODO keep RPCError around? eh idk how much it's used +// TODO should RpcError subclass ValueError? technically the values used in the request somehow were wrong… +// TODO provide a way to see which errors are known in the docs or at tl.telethon.dev + The "iter" variant of the client methods have been removed ---------------------------------------------------------- @@ -385,6 +451,8 @@ However, you're encouraged to change uses of ``.raw_text`` with ``.message``, an either ``.md_text`` or ``.html_text`` as needed. This is because both ``.text`` and ``.raw_text`` may disappear in future versions, and their behaviour is not immediately obvious. +// TODO actually provide the things mentioned here + Using a flat list to define buttons will now create rows and not columns ------------------------------------------------------------------------ diff --git a/setup.py b/setup.py index c82d236d..05f0f7b6 100755 --- a/setup.py +++ b/setup.py @@ -47,7 +47,7 @@ GENERATOR_DIR = Path('telethon_generator') LIBRARY_DIR = Path('telethon') ERRORS_IN = GENERATOR_DIR / 'data/errors.csv' -ERRORS_OUT = LIBRARY_DIR / 'errors/rpcerrorlist.py' +ERRORS_OUT = LIBRARY_DIR / 'errors/_generated.py' METHODS_IN = GENERATOR_DIR / 'data/methods.csv' diff --git a/telethon/_client/chats.py b/telethon/_client/chats.py index ffa06ac7..205113d6 100644 --- a/telethon/_client/chats.py +++ b/telethon/_client/chats.py @@ -676,7 +676,7 @@ async def get_permissions( for participant in chat.full_chat.participants.participants: if participant.user_id == user.user_id: return _custom.ParticipantPermissions(participant, True) - raise errors.UserNotParticipantError(None) + raise errors.USER_NOT_PARTICIPANT(400, 'USER_NOT_PARTICIPANT') raise ValueError('You must pass either a channel or a chat') @@ -694,7 +694,7 @@ async def get_stats( try: req = _tl.fn.stats.GetMessageStats(entity, message) return await self(req) - except errors.StatsMigrateError as e: + except errors.STATS_MIGRATE as e: dc = e.dc else: # Don't bother fetching the Channel entity (costs a request), instead @@ -703,13 +703,13 @@ async def get_stats( try: req = _tl.fn.stats.GetBroadcastStats(entity) return await self(req) - except errors.StatsMigrateError as e: + except errors.STATS_MIGRATE as e: dc = e.dc - except errors.BroadcastRequiredError: + except errors.BROADCAST_REQUIRED: req = _tl.fn.stats.GetMegagroupStats(entity) try: return await self(req) - except errors.StatsMigrateError as e: + except errors.STATS_MIGRATE as e: dc = e.dc sender = await self._borrow_exported_sender(dc) diff --git a/telethon/_client/dialogs.py b/telethon/_client/dialogs.py index 1277a3a4..444a0570 100644 --- a/telethon/_client/dialogs.py +++ b/telethon/_client/dialogs.py @@ -232,7 +232,7 @@ async def delete_dialog( result = await self(_tl.fn.messages.DeleteChatUser( entity.chat_id, _tl.InputUserSelf(), revoke_history=revoke )) - except errors.PeerIdInvalidError: + except errors.PEER_ID_INVALID: # Happens if we didn't have the deactivated information result = None else: diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 06f6bac8..a4349c19 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -12,8 +12,8 @@ from .. import version, helpers, __name__ as __base_name__, _tl from .._crypto import rsa from .._misc import markdown, entitycache, statecache, enums from .._network import MTProtoSender, Connection, ConnectionTcpFull, connection as conns -from ..sessions import Session, SQLiteSession, MemorySession -from ..sessions.types import DataCenter, SessionState +from .._sessions import Session, SQLiteSession, MemorySession +from .._sessions.types import DataCenter, SessionState DEFAULT_DC_ID = 2 DEFAULT_IPV4_IP = '149.154.167.51' diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index 2a03a0b3..da60b24c 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -8,7 +8,8 @@ import traceback import typing import logging -from .. import events, utils, errors, _tl +from .. import events, utils, _tl +from ..errors._rpcbase import RpcError from ..events.common import EventBuilder, EventCommon if typing.TYPE_CHECKING: @@ -244,7 +245,7 @@ async def _dispatch_update(self: 'TelegramClient', update, others, channel_id, p await _get_difference(self, update, channel_id, pts_date) except OSError: pass # We were disconnected, that's okay - except errors.RPCError: + except RpcError: # There's a high chance the request fails because we lack # the channel. Because these "happen sporadically" (#1428) # we should be okay (no flood waits) even if more occur. @@ -418,7 +419,7 @@ async def _handle_auto_reconnect(self: 'TelegramClient'): await self.catch_up() self._log[__name__].info('Successfully fetched missed updates') - except errors.RPCError as e: + except RpcError as e: self._log[__name__].warning('Failed to get missed updates after ' 'reconnect: %r', e) except Exception: diff --git a/telethon/_client/users.py b/telethon/_client/users.py index b653af94..de8a49a3 100644 --- a/telethon/_client/users.py +++ b/telethon/_client/users.py @@ -4,10 +4,11 @@ import itertools import time import typing +from ..errors._custom import MultiError +from ..errors._rpcbase import RpcError, ServerError, FloodError, InvalidDcError, UnauthorizedError from .. import errors, hints, _tl from .._misc import helpers, utils -from ..errors import MultiError, RPCError -from ..sessions.types import Entity +from .._sessions.types import Entity _NOT_A_REQUEST = lambda: TypeError('You can only invoke requests, not types!') @@ -49,7 +50,7 @@ async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sl await asyncio.sleep(diff) self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None) else: - raise errors.FloodWaitError(request=r, capture=diff) + raise errors.FLOOD_WAIT(420, f'FLOOD_WAIT_{diff}', request=r) if self._no_updates: r = _tl.fn.InvokeWithoutUpdates(r) @@ -67,7 +68,7 @@ async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sl for f in future: try: result = await f - except RPCError as e: + except RpcError as e: exceptions.append(e) results.append(None) continue @@ -87,22 +88,20 @@ async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sl if entities: await self.session.insert_entities(entities) return result - except (errors.ServerError, errors.RpcCallFailError, - errors.RpcMcgetFailError, errors.InterdcCallErrorError, - errors.InterdcCallRichErrorError) as e: + except ServerError as e: last_error = e self._log[__name__].warning( 'Telegram is having internal issues %s: %s', e.__class__.__name__, e) await asyncio.sleep(2) - except (errors.FloodWaitError, errors.SlowModeWaitError, errors.FloodTestPhoneWaitError) as e: + except FloodError as e: last_error = e if utils.is_list_like(request): request = request[request_index] - # SLOW_MODE_WAIT is chat-specific, not request-specific - if not isinstance(e, errors.SlowModeWaitError): + # SLOWMODE_WAIT is chat-specific, not request-specific + if not isinstance(e, errors.SLOWMODE_WAIT): self._flood_waited_requests\ [request.CONSTRUCTOR_ID] = time.time() + e.seconds @@ -116,12 +115,11 @@ async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sl await asyncio.sleep(e.seconds) else: raise - except (errors.PhoneMigrateError, errors.NetworkMigrateError, - errors.UserMigrateError) as e: + except InvalidDcError as e: last_error = e self._log[__name__].info('Phone migrated to %d', e.new_dc) should_raise = isinstance(e, ( - errors.PhoneMigrateError, errors.NetworkMigrateError + errors.PHONE_MIGRATE, errors.NETWORK_MIGRATE )) if should_raise and await self.is_user_authorized(): raise @@ -138,7 +136,7 @@ async def get_me(self: 'TelegramClient', input_peer: bool = False) \ try: me = (await self(_tl.fn.users.GetUsers([_tl.InputUserSelf()])))[0] return utils.get_input_peer(me, allow_self=False) if input_peer else me - except errors.UnauthorizedError: + except UnauthorizedError: return None async def is_bot(self: 'TelegramClient') -> bool: @@ -150,7 +148,7 @@ async def is_user_authorized(self: 'TelegramClient') -> bool: # Any request that requires authorization will work await self(_tl.fn.updates.GetState()) self._authorized = True - except errors.RPCError: + except RpcError: self._authorized = False return self._authorized @@ -290,7 +288,7 @@ async def get_input_entity( channels = await self(_tl.fn.channels.GetChannels([ _tl.InputChannel(peer.channel_id, access_hash=0)])) return utils.get_input_peer(channels.chats[0]) - except errors.ChannelInvalidError: + except errors.CHANNEL_INVALID: pass raise ValueError( @@ -338,7 +336,7 @@ async def _get_entity_from_string(self: 'TelegramClient', string): _tl.fn.contacts.GetContacts(0))).users: if user.phone == phone: return user - except errors.BotMethodInvalidError: + except errors.BOT_METHOD_INVALID: raise ValueError('Cannot get entity by phone number as a ' 'bot (try using integer IDs, not strings)') elif string.lower() in ('me', 'self'): @@ -360,7 +358,7 @@ async def _get_entity_from_string(self: 'TelegramClient', string): try: result = await self( _tl.fn.contacts.ResolveUsername(username)) - except errors.UsernameNotOccupiedError as e: + except errors.USERNAME_NOT_OCCUPIED as e: raise ValueError('No user has "{}" as username' .format(username)) from e diff --git a/telethon/_misc/binaryreader.py b/telethon/_misc/binaryreader.py index b0be805b..4117653f 100644 --- a/telethon/_misc/binaryreader.py +++ b/telethon/_misc/binaryreader.py @@ -7,7 +7,7 @@ from datetime import datetime, timezone, timedelta from io import BytesIO from struct import unpack -from ..errors import TypeNotFoundError +from ..errors._custom import TypeNotFoundError from .. import _tl from ..types import _core diff --git a/telethon/_misc/entitycache.py b/telethon/_misc/entitycache.py index 685aa411..a5be14c9 100644 --- a/telethon/_misc/entitycache.py +++ b/telethon/_misc/entitycache.py @@ -3,7 +3,7 @@ import itertools from .._misc import utils from .. import _tl -from ..sessions.types import Entity +from .._sessions.types import Entity # Which updates have the following fields? _has_field = { diff --git a/telethon/_network/authenticator.py b/telethon/_network/authenticator.py index d5b18c56..533eeb5c 100644 --- a/telethon/_network/authenticator.py +++ b/telethon/_network/authenticator.py @@ -8,7 +8,7 @@ from hashlib import sha1 from .. import helpers, _tl from .._crypto import AES, AuthKey, Factorization, rsa -from ..errors import SecurityError +from ..errors._custom import SecurityError from .._misc.binaryreader import BinaryReader diff --git a/telethon/_network/connection/connection.py b/telethon/_network/connection/connection.py index 8bff043d..d206b185 100644 --- a/telethon/_network/connection/connection.py +++ b/telethon/_network/connection/connection.py @@ -13,7 +13,7 @@ try: except ImportError: python_socks = None -from ...errors import InvalidChecksumError +from ...errors._custom import InvalidChecksumError from ... import helpers diff --git a/telethon/_network/connection/tcpfull.py b/telethon/_network/connection/tcpfull.py index 7ebbbe6f..cd60e693 100644 --- a/telethon/_network/connection/tcpfull.py +++ b/telethon/_network/connection/tcpfull.py @@ -2,7 +2,7 @@ import struct from zlib import crc32 from .connection import Connection, PacketCodec -from ...errors import InvalidChecksumError +from ...errors._custom import InvalidChecksumError class FullPacketCodec(PacketCodec): diff --git a/telethon/_network/mtprotoplainsender.py b/telethon/_network/mtprotoplainsender.py index 3a5cffed..427f27ce 100644 --- a/telethon/_network/mtprotoplainsender.py +++ b/telethon/_network/mtprotoplainsender.py @@ -5,7 +5,7 @@ in plain text, when no authorization key has been created yet. import struct from .mtprotostate import MTProtoState -from ..errors import InvalidBufferError +from ..errors._custom import InvalidBufferError from .._misc.binaryreader import BinaryReader diff --git a/telethon/_network/mtprotosender.py b/telethon/_network/mtprotosender.py index 16bd6e32..20e68d72 100644 --- a/telethon/_network/mtprotosender.py +++ b/telethon/_network/mtprotosender.py @@ -4,6 +4,7 @@ import struct from . import authenticator from .._misc.messagepacker import MessagePacker +from ..errors._rpcbase import _mk_error_type from .mtprotoplainsender import MTProtoPlainSender from .requeststate import RequestState from .mtprotostate import MTProtoState @@ -585,12 +586,19 @@ class MTProtoSender: return if rpc_result.error: - error = rpc_message_to_error(rpc_result.error, state.request) self._send_queue.append( RequestState(_tl.MsgsAck([state.msg_id]))) if not state.future.cancelled(): - state.future.set_exception(error) + err_ty = _mk_error_type( + name=rpc_result.error.error_message, + code=rpc_result.error.error_code, + ) + state.future.set_exception(err_ty( + rpc_result.error.error_code, + rpc_result.error.error_message, + state.request + )) else: try: with BinaryReader(rpc_result.body) as reader: diff --git a/telethon/_network/mtprotostate.py b/telethon/_network/mtprotostate.py index f96554ac..19578da3 100644 --- a/telethon/_network/mtprotostate.py +++ b/telethon/_network/mtprotostate.py @@ -4,7 +4,7 @@ import time from hashlib import sha256 from .._crypto import AES -from ..errors import SecurityError, InvalidBufferError +from ..errors._custom import SecurityError, InvalidBufferError from .._misc.binaryreader import BinaryReader from ..types._core import TLMessage, GzipPacked from .._misc.tlobject import TLRequest diff --git a/telethon/errors/__init__.py b/telethon/errors/__init__.py index a50ae36b..152e7823 100644 --- a/telethon/errors/__init__.py +++ b/telethon/errors/__init__.py @@ -1,46 +1,48 @@ -""" -This module holds all the base and automatically generated errors that the -Telegram API has. See telethon_generator/errors.json for more. -""" -import re +import sys -from .common import ( - ReadCancelledError, TypeNotFoundError, InvalidChecksumError, - InvalidBufferError, SecurityError, CdnFileTamperedError, - BadMessageError, MultiError +from ._custom import ( + ReadCancelledError, + TypeNotFoundError, + InvalidChecksumError, + InvalidBufferError, + SecurityError, + CdnFileTamperedError, + BadMessageError, + MultiError, +) +from ._rpcbase import ( + RpcError, + InvalidDcError, + BadRequestError, + UnauthorizedError, + ForbiddenError, + NotFoundError, + AuthKeyError, + FloodError, + ServerError, + BotTimeout, + TimedOutError, + _mk_error_type ) -# This imports the base errors too, as they're imported there -from .rpcbaseerrors import * -from .rpcerrorlist import * +if sys.version_info < (3, 7): + # https://stackoverflow.com/a/7668273/ + class _TelethonErrors: + def __init__(self, _mk_error_type, everything): + self._mk_error_type = _mk_error_type + self.__dict__.update({ + k: v + for k, v in everything.items() + if isinstance(v, type) and issubclass(v, Exception) + }) + def __getattr__(self, name): + return self._mk_error_type(name=name) -def rpc_message_to_error(rpc_error, request): - """ - Converts a Telegram's RPC Error to a Python error. + sys.modules[__name__] = _TelethonErrors(_mk_error_type, globals()) +else: + # https://www.python.org/dev/peps/pep-0562/ + def __getattr__(name): + return _mk_error_type(name=name) - :param rpc_error: the RpcError instance. - :param request: the request that caused this error. - :return: the RPCError as a Python exception that represents this error. - """ - # Try to get the error by direct look-up, otherwise regex - # Case-insensitive, for things like "timeout" which don't conform. - cls = rpc_errors_dict.get(rpc_error.error_message.upper(), None) - if cls: - return cls(request=request) - - for msg_regex, cls in rpc_errors_re: - m = re.match(msg_regex, rpc_error.error_message) - if m: - capture = int(m.group(1)) if m.groups() else None - return cls(request=request, capture=capture) - - # Some errors are negative: - # * -500 for "No workers running", - # * -503 for "Timeout" - # - # We treat them as if they were positive, so -500 will be treated - # as a `ServerError`, etc. - cls = base_errors.get(abs(rpc_error.error_code), RPCError) - return cls(request=request, message=rpc_error.error_message, - code=rpc_error.error_code) +del sys diff --git a/telethon/errors/common.py b/telethon/errors/_custom.py similarity index 100% rename from telethon/errors/common.py rename to telethon/errors/_custom.py diff --git a/telethon/errors/_rpcbase.py b/telethon/errors/_rpcbase.py new file mode 100644 index 00000000..d074be26 --- /dev/null +++ b/telethon/errors/_rpcbase.py @@ -0,0 +1,144 @@ +import re + +from ._generated import _captures, _descriptions +from .. import _tl + + +_NESTS_QUERY = ( + _tl.fn.InvokeAfterMsg, + _tl.fn.InvokeAfterMsgs, + _tl.fn.InitConnection, + _tl.fn.InvokeWithLayer, + _tl.fn.InvokeWithoutUpdates, + _tl.fn.InvokeWithMessagesRange, + _tl.fn.InvokeWithTakeout, +) + + +class RpcError(Exception): + def __init__(self, code, message, request=None): + doc = self.__doc__ + if doc is None: + doc = ( + '\n Please report this error at https://github.com/LonamiWebs/Telethon/issues/3169' + '\n (the library is not aware of it yet and we would appreciate your help, thank you!)' + ) + elif not doc: + doc = '(no description available)' + + super().__init__(f'{message}, code={code}{self._fmt_request(request)}: {doc}') + self.code = code + self.message = message + self.request = request + # Special-case '2fa' to exclude the 2 from values + self.values = [int(x) for x in re.findall(r'-?\d+', re.sub(r'^2fa', '', self.message, flags=re.IGNORECASE))] + self.value = self.values[0] if self.values else None + + @staticmethod + def _fmt_request(request): + if not request: + return '' + + n = 0 + reason = '' + while isinstance(request, _NESTS_QUERY): + n += 1 + reason += request.__class__.__name__ + '(' + request = request.query + reason += request.__class__.__name__ + ')' * n + + return ', request={}'.format(reason) + + def __reduce__(self): + return type(self), (self.request, self.message, self.code) + + +def _mk_error_type(*, name=None, code=None, doc=None, _errors={}) -> type: + if name is None and code is None: + raise ValueError('at least one of `name` or `code` must be provided') + + if name is not None: + # Special-case '2fa' to 'twofa' + name = re.sub(r'^2fa', 'twofa', name, flags=re.IGNORECASE) + + # Get canonical name + name = re.sub(r'[-_\d]', '', name).lower() + while name.endswith('error'): + name = name[:-len('error')] + + doc = _descriptions.get(name, doc) + capture_alias = _captures.get(name) + + d = {'__doc__': doc} + + if capture_alias: + d[capture_alias] = property( + fget=lambda s: s.value, + doc='Alias for `self.value`. Useful to make the code easier to follow.' + ) + + if (name, None) not in _errors: + _errors[(name, None)] = type(f'RpcError{name.title()}', (RpcError,), d) + + if code is not None: + # Pretend negative error codes are positive + code = str(abs(code)) + if (None, code) not in _errors: + _errors[(None, code)] = type(f'RpcError{code}', (RpcError,), {'__doc__': doc}) + + if (name, code) not in _errors: + specific = _errors[(name, None)] + base = _errors[(None, code)] + _errors[(name, code)] = type(f'RpcError{name.title()}{code}', (specific, base), {'__doc__': doc}) + + return _errors[(name, code)] + + +InvalidDcError = _mk_error_type(code=303, doc=""" + The request must be repeated, but directed to a different data center. +""") + +BadRequestError = _mk_error_type(code=400, doc=""" + The query contains errors. In the event that a request was created + using a form and contains user generated data, the user should be + notified that the data must be corrected before the query is repeated. +""") + +UnauthorizedError = _mk_error_type(code=401, doc=""" + There was an unauthorized attempt to use functionality available only + to authorized users. +""") + +ForbiddenError = _mk_error_type(code=403, doc=""" + Privacy violation. For example, an attempt to write a message to + someone who has blacklisted the current user. +""") + +NotFoundError = _mk_error_type(code=404, doc=""" + An attempt to invoke a non-existent object, such as a method. +""") + +AuthKeyError = _mk_error_type(code=406, doc=""" + Errors related to invalid authorization key, like + AUTH_KEY_DUPLICATED which can cause the connection to fail. +""") + +FloodError = _mk_error_type(code=420, doc=""" + The maximum allowed number of attempts to invoke the given method + with the given input parameters has been exceeded. For example, in an + attempt to request a large number of text messages (SMS) for the same + phone number. +""") + +# Witnessed as -500 for "No workers running" +ServerError = _mk_error_type(code=500, doc=""" + An internal server error occurred while a request was being processed + for example, there was a disruption while accessing a database or file + storage. +""") + +# Witnessed as -503 for "Timeout" +BotTimeout = TimedOutError = _mk_error_type(code=503, doc=""" + Clicking the inline buttons of bots that never (or take to long to) + call ``answerCallbackQuery`` will result in this "special" RPCError. +""") diff --git a/telethon/errors/rpcbaseerrors.py b/telethon/errors/rpcbaseerrors.py deleted file mode 100644 index d90bbb02..00000000 --- a/telethon/errors/rpcbaseerrors.py +++ /dev/null @@ -1,131 +0,0 @@ -from .. import _tl - -_NESTS_QUERY = ( - _tl.fn.InvokeAfterMsg, - _tl.fn.InvokeAfterMsgs, - _tl.fn.InitConnection, - _tl.fn.InvokeWithLayer, - _tl.fn.InvokeWithoutUpdates, - _tl.fn.InvokeWithMessagesRange, - _tl.fn.InvokeWithTakeout, -) - -class RPCError(Exception): - """Base class for all Remote Procedure Call errors.""" - code = None - message = None - - def __init__(self, request, message, code=None): - super().__init__('RPCError {}: {}{}'.format( - code or self.code, message, self._fmt_request(request))) - - self.request = request - self.code = code - self.message = message - - @staticmethod - def _fmt_request(request): - n = 0 - reason = '' - while isinstance(request, _NESTS_QUERY): - n += 1 - reason += request.__class__.__name__ + '(' - request = request.query - reason += request.__class__.__name__ + ')' * n - - return ' (caused by {})'.format(reason) - - def __reduce__(self): - return type(self), (self.request, self.message, self.code) - - -class InvalidDCError(RPCError): - """ - The request must be repeated, but directed to a different data center. - """ - code = 303 - message = 'ERROR_SEE_OTHER' - - -class BadRequestError(RPCError): - """ - The query contains errors. In the event that a request was created - using a form and contains user generated data, the user should be - notified that the data must be corrected before the query is repeated. - """ - code = 400 - message = 'BAD_REQUEST' - - -class UnauthorizedError(RPCError): - """ - There was an unauthorized attempt to use functionality available only - to authorized users. - """ - code = 401 - message = 'UNAUTHORIZED' - - -class ForbiddenError(RPCError): - """ - Privacy violation. For example, an attempt to write a message to - someone who has blacklisted the current user. - """ - code = 403 - message = 'FORBIDDEN' - - -class NotFoundError(RPCError): - """ - An attempt to invoke a non-existent object, such as a method. - """ - code = 404 - message = 'NOT_FOUND' - - -class AuthKeyError(RPCError): - """ - Errors related to invalid authorization key, like - AUTH_KEY_DUPLICATED which can cause the connection to fail. - """ - code = 406 - message = 'AUTH_KEY' - - -class FloodError(RPCError): - """ - The maximum allowed number of attempts to invoke the given method - with the given input parameters has been exceeded. For example, in an - attempt to request a large number of text messages (SMS) for the same - phone number. - """ - code = 420 - message = 'FLOOD' - - -class ServerError(RPCError): - """ - An internal server error occurred while a request was being processed - for example, there was a disruption while accessing a database or file - storage. - """ - code = 500 # Also witnessed as -500 - message = 'INTERNAL' - - -class TimedOutError(RPCError): - """ - Clicking the inline buttons of bots that never (or take to long to) - call ``answerCallbackQuery`` will result in this "special" RPCError. - """ - code = 503 # Only witnessed as -503 - message = 'Timeout' - - -BotTimeout = TimedOutError - - -base_errors = {x.code: x for x in ( - InvalidDCError, BadRequestError, UnauthorizedError, ForbiddenError, - NotFoundError, AuthKeyError, FloodError, ServerError, TimedOutError -)} diff --git a/telethon/types/_custom/draft.py b/telethon/types/_custom/draft.py index e81f85ad..82e0cb26 100644 --- a/telethon/types/_custom/draft.py +++ b/telethon/types/_custom/draft.py @@ -1,7 +1,7 @@ import datetime from ... import _tl -from ...errors import RPCError +from ...errors._rpcbase import RpcError from ..._misc import markdown, tlobject from ..._misc.utils import get_input_peer, get_peer @@ -169,7 +169,7 @@ class Draft: def to_dict(self): try: entity = self.entity - except RPCError as e: + except RpcError as e: entity = e return { diff --git a/telethon/types/_custom/message.py b/telethon/types/_custom/message.py index ae0b395e..4d5d615a 100644 --- a/telethon/types/_custom/message.py +++ b/telethon/types/_custom/message.py @@ -6,7 +6,7 @@ from .messagebutton import MessageButton from .forward import Forward from .file import File from ..._misc import utils, tlobject -from ... import errors, _tl +from ... import _tl def _fwd(field, doc): diff --git a/telethon_generator/data/errors.csv b/telethon_generator/data/errors.csv index ff9d8168..71a417d8 100644 --- a/telethon_generator/data/errors.csv +++ b/telethon_generator/data/errors.csv @@ -1,5 +1,5 @@ name,codes,description -2FA_CONFIRM_WAIT_X,420,The account is 2FA protected so it will be deleted in a week. Otherwise it can be reset in {seconds} +2FA_CONFIRM_WAIT_0,420,The account is 2FA protected so it will be deleted in a week. Otherwise it can be reset in {seconds} ABOUT_TOO_LONG,400,The provided bio is too long ACCESS_TOKEN_EXPIRED,400,Bot token expired ACCESS_TOKEN_INVALID,400,The provided token is not valid @@ -101,7 +101,7 @@ DH_G_A_INVALID,400,g_a invalid DOCUMENT_INVALID,400,The document file was invalid and can't be used in inline mode EMAIL_HASH_EXPIRED,400,The email hash expired and cannot be used to verify it EMAIL_INVALID,400,The given email is invalid -EMAIL_UNCONFIRMED_X,400,"Email unconfirmed, the length of the code must be {code_length}" +EMAIL_UNCONFIRMED_0,400,"Email unconfirmed, the length of the code must be {code_length}" EMOJI_INVALID,400, EMOJI_NOT_MODIFIED,400, EMOTICON_EMPTY,400,The emoticon field cannot be empty @@ -124,22 +124,21 @@ FIELD_NAME_INVALID,400,The field with the name FIELD_NAME is invalid FILEREF_UPGRADE_NEEDED,406,The file reference needs to be refreshed before being used again FILE_CONTENT_TYPE_INVALID,400, FILE_ID_INVALID,400,"The provided file id is invalid. Make sure all parameters are present, have the correct type and are not empty (ID, access hash, file reference, thumb size ...)" -FILE_MIGRATE_X,303,The file to be accessed is currently stored in DC {new_dc} +FILE_MIGRATE_0,303,The file to be accessed is currently stored in DC {new_dc} FILE_PARTS_INVALID,400,The number of file parts is invalid -FILE_PART_0_MISSING,400,File part 0 missing FILE_PART_EMPTY,400,The provided file part is empty FILE_PART_INVALID,400,The file part number is invalid FILE_PART_LENGTH_INVALID,400,The length of a file part is invalid FILE_PART_SIZE_CHANGED,400,The file part size (chunk size) cannot change during upload FILE_PART_SIZE_INVALID,400,The provided file part size is invalid -FILE_PART_X_MISSING,400,Part {which} of the file is missing from storage +FILE_PART_0_MISSING,400,Part {which} of the file is missing from storage FILE_REFERENCE_EMPTY,400,The file reference must exist to access the media and it cannot be empty FILE_REFERENCE_EXPIRED,400,The file reference has expired and is no longer valid or it belongs to self-destructing media and cannot be resent FILE_REFERENCE_INVALID,400,The file reference is invalid or you can't do that operation on such message FILE_TITLE_EMPTY,400, FIRSTNAME_INVALID,400,The first name is invalid -FLOOD_TEST_PHONE_WAIT_X,420,A wait of {seconds} seconds is required in the test servers -FLOOD_WAIT_X,420,A wait of {seconds} seconds is required +FLOOD_TEST_PHONE_WAIT_0,420,A wait of {seconds} seconds is required in the test servers +FLOOD_WAIT_0,420,A wait of {seconds} seconds is required FOLDER_ID_EMPTY,400,The folder you tried to delete was already empty FOLDER_ID_INVALID,400,The folder you tried to use was not valid FRESH_CHANGE_ADMINS_FORBIDDEN,400,Recently logged-in users cannot add or change admins @@ -175,8 +174,8 @@ INPUT_LAYER_INVALID,400,The provided layer is invalid INPUT_METHOD_INVALID,400,The invoked method does not exist anymore or has never existed INPUT_REQUEST_TOO_LONG,400,The input request was too long. This may be a bug in the library as it can occur when serializing more bytes than it should (like appending the vector constructor code at the end of a message) INPUT_USER_DEACTIVATED,400,The specified user was deleted -INTERDC_X_CALL_ERROR,500,An error occurred while communicating with DC {dc} -INTERDC_X_CALL_RICH_ERROR,500,A rich error occurred while communicating with DC {dc} +INTERDC_0_CALL_ERROR,500,An error occurred while communicating with DC {dc} +INTERDC_0_CALL_RICH_ERROR,500,A rich error occurred while communicating with DC {dc} INVITE_HASH_EMPTY,400,The invite hash is empty INVITE_HASH_EXPIRED,400,The chat the user tried to join has expired and is not valid anymore INVITE_HASH_INVALID,400,The invite hash is invalid @@ -218,7 +217,7 @@ MT_SEND_QUEUE_TOO_LONG,500, MULTI_MEDIA_TOO_LONG,400,Too many media files were included in the same album NEED_CHAT_INVALID,500,The provided chat is invalid NEED_MEMBER_INVALID,500,The provided member is invalid or does not exist (for example a thumb size) -NETWORK_MIGRATE_X,303,The source IP address is associated with DC {new_dc} +NETWORK_MIGRATE_0,303,The source IP address is associated with DC {new_dc} NEW_SALT_INVALID,400,The new salt is invalid NEW_SETTINGS_INVALID,400,The new settings are invalid NEXT_OFFSET_INVALID,400,The value for next_offset is invalid. Check that it has normal characters and is not too long @@ -238,7 +237,7 @@ PASSWORD_MISSING,400,The account must have 2-factor authentication enabled (a pa PASSWORD_RECOVERY_EXPIRED,400, PASSWORD_RECOVERY_NA,400, PASSWORD_REQUIRED,400,The account must have 2-factor authentication enabled (a password) before this method can be used -PASSWORD_TOO_FRESH_X,400,The password was added too recently and {seconds} seconds must pass before using the method +PASSWORD_TOO_FRESH_0,400,The password was added too recently and {seconds} seconds must pass before using the method PAYMENT_PROVIDER_INVALID,400,The payment provider was not recognised or its token was invalid PEER_FLOOD,400,Too many requests PEER_ID_INVALID,400,"An invalid Peer was used. Make sure to pass the right peer type and that the value is valid (for instance, bots cannot start conversations)" @@ -250,7 +249,7 @@ PHONE_CODE_EMPTY,400,The phone code is missing PHONE_CODE_EXPIRED,400,The confirmation code has expired PHONE_CODE_HASH_EMPTY,400,The phone code hash is missing PHONE_CODE_INVALID,400,The phone code entered was invalid -PHONE_MIGRATE_X,303,The phone number a user is trying to use for authorization is associated with DC {new_dc} +PHONE_MIGRATE_0,303,The phone number a user is trying to use for authorization is associated with DC {new_dc} PHONE_NUMBER_APP_SIGNUP_FORBIDDEN,400,You can't sign up using this app PHONE_NUMBER_BANNED,400,The used phone number has been banned from Telegram and cannot be used anymore. Maybe check https://www.telegram.org/faq_spam PHONE_NUMBER_FLOOD,400,You asked for the code too many times. @@ -276,7 +275,7 @@ POLL_OPTION_INVALID,400,A poll option used invalid data (the data may be too lon POLL_QUESTION_INVALID,400,The poll question was either empty or too long POLL_UNSUPPORTED,400,This layer does not support polls in the issued method POLL_VOTE_REQUIRED,403, -PREVIOUS_CHAT_IMPORT_ACTIVE_WAIT_XMIN,406,"Similar to a flood wait, must wait {minutes} minutes" +PREVIOUS_CHAT_IMPORT_ACTIVE_WAIT_0MIN,406,"Similar to a flood wait, must wait {minutes} minutes" PRIVACY_KEY_INVALID,400,The privacy key is invalid PRIVACY_TOO_LONG,400,Cannot add that many entities in a single request PRIVACY_VALUE_INVALID,400,The privacy value is invalid @@ -322,16 +321,16 @@ SENSITIVE_CHANGE_FORBIDDEN,403,Your sensitive content settings cannot be changed SESSION_EXPIRED,401,The authorization has expired SESSION_PASSWORD_NEEDED,401,Two-steps verification is enabled and a password is required SESSION_REVOKED,401,"The authorization has been invalidated, because of the user terminating all sessions" -SESSION_TOO_FRESH_X,400,The session logged in too recently and {seconds} seconds must pass before calling the method +SESSION_TOO_FRESH_0,400,The session logged in too recently and {seconds} seconds must pass before calling the method SHA256_HASH_INVALID,400,The provided SHA256 hash is invalid SHORTNAME_OCCUPY_FAILED,400,An error occurred when trying to register the short-name used for the sticker pack. Try a different name SHORT_NAME_INVALID,400, SHORT_NAME_OCCUPIED,400, -SLOWMODE_WAIT_X,420,A wait of {seconds} seconds is required before sending another message in this chat +SLOWMODE_WAIT_0,420,A wait of {seconds} seconds is required before sending another message in this chat SRP_ID_INVALID,400, START_PARAM_EMPTY,400,The start parameter is empty START_PARAM_INVALID,400,Start parameter invalid -STATS_MIGRATE_X,303,The channel statistics must be fetched from DC {dc} +STATS_MIGRATE_0,303,The channel statistics must be fetched from DC {dc} STICKERSET_INVALID,400,The provided sticker set is invalid STICKERSET_OWNER_ANONYMOUS,406,This sticker set can't be used as the group's official stickers because it was created by one of its anonymous admins STICKERS_EMPTY,400,No sticker provided @@ -348,7 +347,7 @@ STICKER_THUMB_PNG_NOPNG,400,Stickerset thumb must be a png file but the used fil STICKER_THUMB_TGS_NOTGS,400,Stickerset thumb must be a tgs file but the used file was not tgs STORAGE_CHECK_FAILED,500,Server storage check failed STORE_INVALID_SCALAR_TYPE,500, -TAKEOUT_INIT_DELAY_X,420,A wait of {seconds} seconds is required before being able to initiate the takeout +TAKEOUT_INIT_DELAY_0,420,A wait of {seconds} seconds is required before being able to initiate the takeout TAKEOUT_INVALID,400,The takeout session has been invalidated by another data export session TAKEOUT_REQUIRED,400,You must initialize a takeout request first TEMP_AUTH_KEY_EMPTY,400,No temporary auth key provided @@ -391,7 +390,7 @@ USER_INVALID,400,The given user was invalid USER_IS_BLOCKED,400 403,User is blocked USER_IS_BOT,400,Bots can't send messages to other bots USER_KICKED,400,This user was kicked from this supergroup/channel -USER_MIGRATE_X,303,The user whose identity is being used to execute queries is associated with DC {new_dc} +USER_MIGRATE_0,303,The user whose identity is being used to execute queries is associated with DC {new_dc} USER_NOT_MUTUAL_CONTACT,400 403,The provided user is not a mutual contact USER_NOT_PARTICIPANT,400,The target user is not a member of the specified megagroup or channel USER_PRIVACY_RESTRICTED,403,The user's privacy settings do not allow you to do this diff --git a/telethon_generator/data/methods.csv b/telethon_generator/data/methods.csv index 4427492b..135d5618 100644 --- a/telethon_generator/data/methods.csv +++ b/telethon_generator/data/methods.csv @@ -7,7 +7,7 @@ account.confirmPasswordEmail,user, account.confirmPhone,user,CODE_HASH_INVALID PHONE_CODE_EMPTY account.createTheme,user,THEME_MIME_INVALID account.declinePasswordReset,user,RESET_REQUEST_MISSING -account.deleteAccount,user,2FA_CONFIRM_WAIT_X +account.deleteAccount,user,2FA_CONFIRM_WAIT_0 account.deleteSecureValue,user, account.finishTakeoutSession,user, account.getAccountTTL,user, @@ -57,7 +57,7 @@ account.setPrivacy,user,PRIVACY_KEY_INVALID PRIVACY_TOO_LONG account.unregisterDevice,user,TOKEN_INVALID account.updateDeviceLocked,user, account.updateNotifySettings,user,PEER_ID_INVALID -account.updatePasswordSettings,user,EMAIL_UNCONFIRMED_X NEW_SALT_INVALID NEW_SETTINGS_INVALID PASSWORD_HASH_INVALID +account.updatePasswordSettings,user,EMAIL_UNCONFIRMED_0 NEW_SALT_INVALID NEW_SETTINGS_INVALID PASSWORD_HASH_INVALID account.updateProfile,user,ABOUT_TOO_LONG FIRSTNAME_INVALID account.updateStatus,user,SESSION_PASSWORD_NEEDED account.updateTheme,user,THEME_INVALID @@ -97,7 +97,7 @@ channels.deleteMessages,both,CHANNEL_INVALID CHANNEL_PRIVATE MESSAGE_DELETE_FORB channels.deleteUserHistory,user,CHANNEL_INVALID CHAT_ADMIN_REQUIRED channels.editAdmin,both,ADMINS_TOO_MUCH ADMIN_RANK_EMOJI_NOT_ALLOWED ADMIN_RANK_INVALID BOT_CHANNELS_NA CHANNEL_INVALID CHAT_ADMIN_INVITE_REQUIRED CHAT_ADMIN_REQUIRED FRESH_CHANGE_ADMINS_FORBIDDEN RIGHT_FORBIDDEN USER_CREATOR USER_ID_INVALID USER_NOT_MUTUAL_CONTACT USER_PRIVACY_RESTRICTED channels.editBanned,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED USER_ADMIN_INVALID USER_ID_INVALID -channels.editCreator,user,PASSWORD_MISSING PASSWORD_TOO_FRESH_X SESSION_TOO_FRESH_X SRP_ID_INVALID +channels.editCreator,user,PASSWORD_MISSING PASSWORD_TOO_FRESH_0 SESSION_TOO_FRESH_0 SRP_ID_INVALID channels.editLocation,user, channels.editPhoto,both,CHANNEL_INVALID CHAT_ADMIN_REQUIRED FILE_REFERENCE_INVALID PHOTO_INVALID channels.editTitle,both,CHANNEL_INVALID CHAT_ADMIN_REQUIRED CHAT_NOT_MODIFIED @@ -251,7 +251,7 @@ messages.getWebPage,user,WC_CONVERT_URL_INVALID messages.getWebPagePreview,user, messages.hidePeerSettingsBar,user, messages.importChatInvite,user,CHANNELS_TOO_MUCH INVITE_HASH_EMPTY INVITE_HASH_EXPIRED INVITE_HASH_INVALID SESSION_PASSWORD_NEEDED USERS_TOO_MUCH USER_ALREADY_PARTICIPANT -messages.initHistoryImport,user,IMPORT_FILE_INVALID IMPORT_FORMAT_UNRECOGNIZED PREVIOUS_CHAT_IMPORT_ACTIVE_WAIT_XMIN TIMEOUT +messages.initHistoryImport,user,IMPORT_FILE_INVALID IMPORT_FORMAT_UNRECOGNIZED PREVIOUS_CHAT_IMPORT_ACTIVE_WAIT_0MIN TIMEOUT messages.installStickerSet,user,STICKERSET_INVALID messages.markDialogUnread,user, messages.migrateChat,user,CHAT_ADMIN_REQUIRED CHAT_ID_INVALID PEER_ID_INVALID @@ -337,8 +337,8 @@ reqPq,both, reqPqMulti,both, rpcDropAnswer,both, setClientDHParams,both, -stats.getBroadcastStats,user,BROADCAST_REQUIRED CHAT_ADMIN_REQUIRED CHP_CALL_FAIL STATS_MIGRATE_X -stats.getMegagroupStats,user,CHAT_ADMIN_REQUIRED MEGAGROUP_REQUIRED STATS_MIGRATE_X +stats.getBroadcastStats,user,BROADCAST_REQUIRED CHAT_ADMIN_REQUIRED CHP_CALL_FAIL STATS_MIGRATE_0 +stats.getMegagroupStats,user,CHAT_ADMIN_REQUIRED MEGAGROUP_REQUIRED STATS_MIGRATE_0 stats.loadAsyncGraph,user,GRAPH_INVALID_RELOAD GRAPH_OUTDATED_RELOAD stickers.addStickerToSet,bot,BOT_MISSING STICKERSET_INVALID STICKER_PNG_NOPNG STICKER_TGS_NOTGS stickers.changeStickerPosition,bot,BOT_MISSING STICKER_INVALID diff --git a/telethon_generator/generators/errors.py b/telethon_generator/generators/errors.py index 386575be..5771369c 100644 --- a/telethon_generator/generators/errors.py +++ b/telethon_generator/generators/errors.py @@ -1,60 +1,12 @@ def generate_errors(errors, f): - # Exact/regex match to create {CODE: ErrorClassName} - exact_match = [] - regex_match = [] - - # Find out what subclasses to import and which to create - import_base, create_base = set(), {} + f.write('_captures = {\n') for error in errors: - if error.subclass_exists: - import_base.add(error.subclass) - else: - create_base[error.subclass] = error.int_code + if error.capture_name: + f.write(f" {error.canonical_name!r}: {error.capture_name!r},\n") + f.write('}\n') - if error.has_captures: - regex_match.append(error) - else: - exact_match.append(error) - - # Imports and new subclass creation - f.write('from .rpcbaseerrors import RPCError, {}\n' - .format(", ".join(sorted(import_base)))) - - for cls, int_code in sorted(create_base.items(), key=lambda t: t[1]): - f.write('\n\nclass {}(RPCError):\n code = {}\n' - .format(cls, int_code)) - - # Error classes generation + f.write('\n\n_descriptions = {\n') for error in errors: - f.write('\n\nclass {}({}):\n '.format(error.name, error.subclass)) - - if error.has_captures: - f.write('def __init__(self, request, capture=0):\n ' - ' self.request = request\n ') - f.write(' self.{} = int(capture)\n ' - .format(error.capture_name)) - else: - f.write('def __init__(self, request):\n ' - ' self.request = request\n ') - - f.write('super(Exception, self).__init__(' - '{}'.format(repr(error.description))) - - if error.has_captures: - f.write('.format({0}=self.{0})'.format(error.capture_name)) - - f.write(' + self._fmt_request(self.request))\n\n') - f.write(' def __reduce__(self):\n ') - if error.has_captures: - f.write('return type(self), (self.request, self.{})\n'.format(error.capture_name)) - else: - f.write('return type(self), (self.request,)\n') - - # Create the actual {CODE: ErrorClassName} dict once classes are defined - f.write('\n\nrpc_errors_dict = {\n') - for error in exact_match: - f.write(' {}: {},\n'.format(repr(error.pattern), error.name)) - f.write('}\n\nrpc_errors_re = (\n') - for error in regex_match: - f.write(' ({}, {}),\n'.format(repr(error.pattern), error.name)) - f.write(')\n') + if error.description: + f.write(f" {error.canonical_name!r}: {error.description!r},\n") + f.write('}\n') diff --git a/telethon_generator/parsers/errors.py b/telethon_generator/parsers/errors.py index 04cd3412..0982edea 100644 --- a/telethon_generator/parsers/errors.py +++ b/telethon_generator/parsers/errors.py @@ -17,25 +17,16 @@ KNOWN_BASE_CLASSES = { } -def _get_class_name(error_code): +def _get_canonical_name(error_code): """ - Gets the corresponding class name for the given error code, - this either being an integer (thus base error name) or str. + Gets the corresponding canonical name for the given error code. """ - if isinstance(error_code, int): - return KNOWN_BASE_CLASSES.get( - abs(error_code), 'RPCError' + str(error_code).replace('-', 'Neg') - ) + # This code should match that of the library itself. + name = re.sub(r'[-_\d]', '', error_code).lower() + while name.endswith('error'): + name = name[:-len('error')] - if error_code.startswith('2'): - error_code = re.sub(r'2', 'TWO_', error_code, count=1) - - if re.match(r'\d+', error_code): - raise RuntimeError('error code starting with a digit cannot have valid Python name: {}'.format(error_code)) - - return snake_to_camel_case( - error_code.replace('FIRSTNAME', 'FIRST_NAME')\ - .replace('SLOWMODE', 'SLOW_MODE').lower(), suffix='Error') + return name class Error: @@ -45,18 +36,13 @@ class Error: # Telegram isn't exactly consistent with returned errors anyway. self.int_code = codes[0] self.str_code = name - self.subclass = _get_class_name(codes[0]) - self.subclass_exists = abs(codes[0]) in KNOWN_BASE_CLASSES + self.canonical_name = _get_canonical_name(name) self.description = description - self.has_captures = '_X' in name - if self.has_captures: - self.name = _get_class_name(name.replace('_X', '_')) - self.pattern = name.replace('_X', r'_(\d+)') + has_captures = '0' in name + if has_captures: self.capture_name = re.search(r'{(\w+)}', description).group(1) else: - self.name = _get_class_name(name) - self.pattern = name self.capture_name = None From ce292b36abc29999b86fd9dd595c617b6cc6a7a3 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 24 Sep 2021 20:12:23 +0200 Subject: [PATCH 094/256] Fix GROUP check in EntityCache --- telethon/_misc/entitycache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/_misc/entitycache.py b/telethon/_misc/entitycache.py index a5be14c9..87bde9fa 100644 --- a/telethon/_misc/entitycache.py +++ b/telethon/_misc/entitycache.py @@ -100,7 +100,7 @@ class EntityCache: if id not in self.__dict__: if ty in (Entity.USER, Entity.BOT): self.__dict__[id] = _tl.InputPeerUser(id, access_hash) - elif ty in (Entity.GROUP): + elif ty in (Entity.GROUP,): self.__dict__[id] = _tl.InputPeerChat(id) elif ty in (Entity.CHANNEL, Entity.MEGAGROUP, Entity.GIGAGROUP): self.__dict__[id] = _tl.InputPeerChannel(id, access_hash) From 5a44510e2dc65bc7829a83290f6a28bfca9105fd Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 24 Sep 2021 20:46:33 +0200 Subject: [PATCH 095/256] Forward client calls to impl in a more straightforward manner --- telethon/_client/telegramclient.py | 168 +++++++++++++++-------------- 1 file changed, 87 insertions(+), 81 deletions(-) diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 15a7db75..54ab9710 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -15,6 +15,12 @@ from ..events.common import EventBuilder, EventCommon from .._misc import enums +def forward_call(to_func): + def decorator(from_func): + return functools.wraps(from_func)(to_func) + return decorator + + class TelegramClient: """ Arguments @@ -165,6 +171,7 @@ class TelegramClient: # region Account + @forward_call(account.takeout) def takeout( self: 'TelegramClient', finalize: bool = True, @@ -256,8 +263,8 @@ class TelegramClient: except errors.TakeoutInitDelayError as e: print('Must wait', e.seconds, 'before takeout') """ - return account.takeout(**locals()) + @forward_call(account.end_takeout) async def end_takeout(self: 'TelegramClient', success: bool) -> bool: """ Finishes the current takeout session. @@ -274,12 +281,12 @@ class TelegramClient: await client.end_takeout(success=False) """ - return await account.end_takeout(**locals()) # endregion Account # region Auth + @forward_call(auth.start) def start( self: 'TelegramClient', phone: typing.Callable[[], str] = lambda: input('Please enter your phone (or bot token): '), @@ -365,8 +372,8 @@ class TelegramClient: async with client.start(): pass """ - return auth.start(**locals()) + @forward_call(auth.sign_in) async def sign_in( self: 'TelegramClient', phone: str = None, @@ -422,8 +429,8 @@ class TelegramClient: code = input('enter code: ') await client.sign_in(phone, code) """ - return await auth.sign_in(**locals()) + @forward_call(auth.sign_up) async def sign_up( self: 'TelegramClient', code: typing.Union[str, int], @@ -474,8 +481,8 @@ class TelegramClient: code = input('enter code: ') await client.sign_up(code, first_name='Anna', last_name='Banana') """ - return await auth.sign_up(**locals()) + @forward_call(auth.send_code_request) async def send_code_request( self: 'TelegramClient', phone: str, @@ -501,8 +508,8 @@ class TelegramClient: sent = await client.send_code_request(phone) print(sent) """ - return await auth.send_code_request(**locals()) + @forward_call(auth.qr_login) async def qr_login(self: 'TelegramClient', ignored_ids: typing.List[int] = None) -> _custom.QRLogin: """ Initiates the QR login procedure. @@ -536,8 +543,8 @@ class TelegramClient: # Important! You need to wait for the login to complete! await qr_login.wait() """ - return await auth.qr_login(**locals()) + @forward_call(auth.log_out) async def log_out(self: 'TelegramClient') -> bool: """ Logs out Telegram and deletes the current ``*.session`` file. @@ -551,8 +558,8 @@ class TelegramClient: # Note: you will need to login again! await client.log_out() """ - return await auth.log_out(**locals()) + @forward_call(auth.edit_2fa) async def edit_2fa( self: 'TelegramClient', current_password: str = None, @@ -614,7 +621,6 @@ class TelegramClient: # Removing the password await client.edit_2fa(current_password='I_<3_Telethon') """ - return await auth.edit_2fa(**locals()) async def __aenter__(self): await self.connect() @@ -627,6 +633,7 @@ class TelegramClient: # region Bots + @forward_call(bots.inline_query) async def inline_query( self: 'TelegramClient', bot: 'hints.EntityLike', @@ -674,7 +681,6 @@ class TelegramClient: # Send the first result to some chat message = await results[0].click('TelethonOffTopic') """ - return await bots.inline_query(**locals()) # endregion Bots @@ -720,6 +726,7 @@ class TelegramClient: # region Chats + @forward_call(chats.get_participants) def get_participants( self: 'TelegramClient', entity: 'hints.EntityLike', @@ -789,8 +796,8 @@ class TelegramClient: users = await client.get_participants(chat, limit=0) print(users.total) """ - return chats.get_participants(**locals()) + @forward_call(chats.get_admin_log) def get_admin_log( self: 'TelegramClient', entity: 'hints.EntityLike', @@ -924,8 +931,8 @@ class TelegramClient: # Print the old message before it was deleted print(events[-1].old) """ - return chats.get_admin_log(**locals()) + @forward_call(chats.get_profile_photos) def get_profile_photos( self: 'TelegramClient', entity: 'hints.EntityLike', @@ -972,8 +979,8 @@ class TelegramClient: photos = await client.get_profile_photos(channel, limit=None) await client.download_media(photos[-1]) """ - return chats.get_profile_photos(**locals()) + @forward_call(chats.action) def action( self: 'TelegramClient', entity: 'hints.EntityLike', @@ -1050,8 +1057,8 @@ class TelegramClient: async with client.action(chat, 'document') as action: await client.send_file(chat, zip_file, progress_callback=action.progress) """ - return chats.action(**locals()) + @forward_call(chats.edit_admin) async def edit_admin( self: 'TelegramClient', entity: 'hints.EntityLike', @@ -1156,8 +1163,8 @@ class TelegramClient: # Granting all permissions except for `add_admins` await client.edit_admin(chat, user, is_admin=True, add_admins=False) """ - return await chats.edit_admin(**locals()) + @forward_call(chats.edit_permissions) async def edit_permissions( self: 'TelegramClient', entity: 'hints.EntityLike', @@ -1273,8 +1280,8 @@ class TelegramClient: await client.edit_permissions(chat, user, view_messages=False) await client.edit_permissions(chat, user) """ - return await chats.edit_permissions(**locals()) + @forward_call(chats.kick_participant) async def kick_participant( self: 'TelegramClient', entity: 'hints.EntityLike', @@ -1312,8 +1319,8 @@ class TelegramClient: # Leaving chat await client.kick_participant(chat, 'me') """ - return await chats.kick_participant(**locals()) + @forward_call(chats.get_permissions) async def get_permissions( self: 'TelegramClient', entity: 'hints.EntityLike', @@ -1350,8 +1357,8 @@ class TelegramClient: # Get Banned Permissions of Chat await client.get_permissions(chat) """ - return await chats.get_permissions(**locals()) + @forward_call(chats.get_stats) async def get_stats( self: 'TelegramClient', entity: 'hints.EntityLike', @@ -1396,12 +1403,12 @@ class TelegramClient: .. _`at least 500 members`: https://telegram.org/blog/profile-videos-people-nearby-and-more """ - return await chats.get_stats(**locals()) # endregion Chats # region Dialogs + @forward_call(dialogs.get_dialogs) def get_dialogs( self: 'TelegramClient', limit: float = (), @@ -1497,8 +1504,8 @@ class TelegramClient: archived = await client.get_dialogs(folder=1, limit=None) archived = await client.get_dialogs(archived=True, limit=None) """ - return dialogs.get_dialogs(**locals()) + @forward_call(dialogs.get_drafts) def get_drafts( self: 'TelegramClient', entity: 'hints.EntitiesLike' = None @@ -1531,8 +1538,8 @@ class TelegramClient: draft = await client.get_drafts('me') print(draft.text) """ - return dialogs.get_drafts(**locals()) + @forward_call(dialogs.edit_folder) async def edit_folder( self: 'TelegramClient', entity: 'hints.EntitiesLike' = None, @@ -1590,8 +1597,8 @@ class TelegramClient: # Un-archiving all dialogs await client.edit_folder(unpack=1) """ - return await dialogs.edit_folder(**locals()) + @forward_call(dialogs.delete_dialog) async def delete_dialog( self: 'TelegramClient', entity: 'hints.EntityLike', @@ -1635,12 +1642,12 @@ class TelegramClient: # Leaving a channel by username await client.delete_dialog('username') """ - return await dialogs.delete_dialog(**locals()) # endregion Dialogs # region Downloads + @forward_call(downloads.download_profile_photo) async def download_profile_photo( self: 'TelegramClient', entity: 'hints.EntityLike', @@ -1684,8 +1691,8 @@ class TelegramClient: path = await client.download_profile_photo('me') print(path) """ - return await downloads.download_profile_photo(**locals()) + @forward_call(downloads.download_media) async def download_media( self: 'TelegramClient', message: 'hints.MessageLike', @@ -1760,8 +1767,8 @@ class TelegramClient: await client.download_media(message, progress_callback=callback) """ - return await downloads.download_media(**locals()) + @forward_call(downloads.iter_download) def iter_download( self: 'TelegramClient', file: 'hints.FileLike', @@ -1857,7 +1864,6 @@ class TelegramClient: await stream.close() assert len(header) == 32 """ - return downloads.iter_download(**locals()) # endregion Downloads @@ -1907,6 +1913,7 @@ class TelegramClient: # region Messages + @forward_call(messages.get_messages) def get_messages( self: 'TelegramClient', entity: 'hints.EntityLike', @@ -2104,8 +2111,8 @@ class TelegramClient: # Get a single message given an ID: message_1337 = await client.get_messages(chat, ids=1337) """ - return messages.get_messages(**locals()) + @forward_call(messages.send_message) async def send_message( self: 'TelegramClient', entity: 'hints.EntityLike', @@ -2298,8 +2305,8 @@ class TelegramClient: from datetime import timedelta await client.send_message(chat, 'Hi, future!', schedule=timedelta(minutes=5)) """ - return await messages.send_message(**locals()) + @forward_call(messages.forward_messages) async def forward_messages( self: 'TelegramClient', entity: 'hints.EntityLike', @@ -2380,19 +2387,8 @@ class TelegramClient: # Forwarding as a copy await client.send_message(chat, message) """ - from . import messages as m - return await m.forward_messages( - self=self, - entity=entity, - messages=messages, - from_peer=from_peer, - background=background, - with_my_score=with_my_score, - silent=silent, - as_album=as_album, - schedule=schedule - ) + @forward_call(messages.edit_message) async def edit_message( self: 'TelegramClient', entity: 'typing.Union[hints.EntityLike, _tl.Message]', @@ -2520,8 +2516,8 @@ class TelegramClient: # or await message.edit('hello!!!') """ - return await messages.edit_message(**locals()) + @forward_call(messages.delete_messages) async def delete_messages( self: 'TelegramClient', entity: 'hints.EntityLike', @@ -2572,8 +2568,8 @@ class TelegramClient: await client.delete_messages(chat, messages) """ - return await messages.delete_messages(**locals()) + @forward_call(messages.send_read_acknowledge) async def send_read_acknowledge( self: 'TelegramClient', entity: 'hints.EntityLike', @@ -2624,8 +2620,8 @@ class TelegramClient: # ...or passing a list of messages to mark as read await client.send_read_acknowledge(chat, messages) """ - return await messages.send_read_acknowledge(**locals()) + @forward_call(messages.pin_message) async def pin_message( self: 'TelegramClient', entity: 'hints.EntityLike', @@ -2665,8 +2661,8 @@ class TelegramClient: message = await client.send_message(chat, 'Pinotifying is fun!') await client.pin_message(chat, message, notify=True) """ - return await messages.pin_message(**locals()) + @forward_call(messages.unpin_message) async def unpin_message( self: 'TelegramClient', entity: 'hints.EntityLike', @@ -2695,7 +2691,6 @@ class TelegramClient: # Unpin all messages from a chat await client.unpin_message(chat) """ - return await messages.unpin_message(**locals()) # endregion Messages @@ -2731,7 +2726,7 @@ class TelegramClient: base_logger: typing.Union[str, logging.Logger] = None, receive_updates: bool = True ): - return telegrambaseclient.init(**locals()) + telegrambaseclient.init(**locals()) @property def loop(self: 'TelegramClient') -> asyncio.AbstractEventLoop: @@ -2760,6 +2755,7 @@ class TelegramClient: def flood_sleep_threshold(self, value): return telegrambaseclient.set_flood_sleep_threshold(**locals()) + @forward_call(telegrambaseclient.connect) async def connect(self: 'TelegramClient') -> None: """ Connects to Telegram. @@ -2782,8 +2778,8 @@ class TelegramClient: except OSError: print('Failed to connect') """ - return await telegrambaseclient.connect(**locals()) + @forward_call(telegrambaseclient.is_connected) def is_connected(self: 'TelegramClient') -> bool: """ Returns `True` if the user has connected. @@ -2796,8 +2792,8 @@ class TelegramClient: while client.is_connected(): await asyncio.sleep(1) """ - return telegrambaseclient.is_connected(**locals()) + @forward_call(telegrambaseclient.disconnect) def disconnect(self: 'TelegramClient'): """ Disconnects from Telegram. @@ -2812,8 +2808,8 @@ class TelegramClient: # You don't need to use this if you used "with client" await client.disconnect() """ - return telegrambaseclient.disconnect(**locals()) + @forward_call(telegrambaseclient.set_proxy) def set_proxy(self: 'TelegramClient', proxy: typing.Union[tuple, dict]): """ Changes the proxy which will be used on next (re)connection. @@ -2824,12 +2820,12 @@ class TelegramClient: - on a call `await client.connect()` (after complete disconnect) - on auto-reconnect attempt (e.g, after previous connection was lost) """ - return telegrambaseclient.set_proxy(**locals()) # endregion Base # region Updates + @forward_call(updates.set_receive_updates) async def set_receive_updates(self: 'TelegramClient', receive_updates): """ Change the value of `receive_updates`. @@ -2837,8 +2833,8 @@ class TelegramClient: This is an `async` method, because in order for Telegram to start sending updates again, a request must be made. """ - return await updates.set_receive_updates(**locals()) + @forward_call(updates.run_until_disconnected) def run_until_disconnected(self: 'TelegramClient'): """ Wait until the library is disconnected. @@ -2870,8 +2866,8 @@ class TelegramClient: # script from exiting. await client.run_until_disconnected() """ - return updates.run_until_disconnected(**locals()) + @forward_call(updates.on) def on(self: 'TelegramClient', event: EventBuilder): """ Decorator used to `add_event_handler` more conveniently. @@ -2893,8 +2889,8 @@ class TelegramClient: async def handler(event): ... """ - return updates.on(**locals()) + @forward_call(updates.add_event_handler) def add_event_handler( self: 'TelegramClient', callback: updates.Callback, @@ -2931,8 +2927,8 @@ class TelegramClient: client.add_event_handler(handler, events.NewMessage) """ - return updates.add_event_handler(**locals()) + @forward_call(updates.remove_event_handler) def remove_event_handler( self: 'TelegramClient', callback: updates.Callback, @@ -2958,8 +2954,8 @@ class TelegramClient: # "handler" will stop receiving anything client.remove_event_handler(handler) """ - return updates.remove_event_handler(**locals()) + @forward_call(updates.list_event_handlers) def list_event_handlers(self: 'TelegramClient')\ -> 'typing.Sequence[typing.Tuple[Callback, EventBuilder]]': """ @@ -2979,8 +2975,8 @@ class TelegramClient: for callback, event in client.list_event_handlers(): print(id(callback), type(event)) """ - return updates.list_event_handlers(**locals()) + @forward_call(updates.catch_up) async def catch_up(self: 'TelegramClient'): """ "Catches up" on the missed updates while the client was offline. @@ -2994,12 +2990,12 @@ class TelegramClient: await client.catch_up() """ - return await updates.catch_up(**locals()) # endregion Updates # region Uploads + @forward_call(uploads.send_file) async def send_file( self: 'TelegramClient', entity: 'hints.EntityLike', @@ -3239,8 +3235,8 @@ class TelegramClient: vcard='' )) """ - return await uploads.send_file(**locals()) + @forward_call(uploads.upload_file) async def upload_file( self: 'TelegramClient', file: 'hints.FileLike', @@ -3326,12 +3322,12 @@ class TelegramClient: await client.send_file(chat, file) # sends as song await client.send_file(chat, file, voice_note=True) # sends as voice note """ - return await uploads.upload_file(**locals()) # endregion Uploads # region Users + @forward_call(users.call) async def __call__(self: 'TelegramClient', request, ordered=False, flood_sleep_threshold=None): """ Invokes (sends) one or more MTProtoRequests and returns (receives) @@ -3355,8 +3351,8 @@ class TelegramClient: The result of the request (often a `TLObject`) or a list of results if more than one request was given. """ - return await users.call(**locals()) + @forward_call(users.get_me) async def get_me(self: 'TelegramClient', input_peer: bool = False) \ -> 'typing.Union[_tl.User, _tl.InputPeerUser]': """ @@ -3379,8 +3375,8 @@ class TelegramClient: me = await client.get_me() print(me.username) """ - return await users.get_me(**locals()) + @forward_call(users.is_bot) async def is_bot(self: 'TelegramClient') -> bool: """ Return `True` if the signed-in user is a bot, `False` otherwise. @@ -3393,8 +3389,8 @@ class TelegramClient: else: print('Hello') """ - return await users.is_bot(**locals()) + @forward_call(users.is_user_authorized) async def is_user_authorized(self: 'TelegramClient') -> bool: """ Returns `True` if the user is authorized (logged in). @@ -3407,8 +3403,8 @@ class TelegramClient: code = input('enter code: ') await client.sign_in(phone, code) """ - return await users.is_user_authorized(**locals()) + @forward_call(users.get_entity) async def get_entity( self: 'TelegramClient', entity: 'hints.EntitiesLike') -> 'hints.Entity': @@ -3465,8 +3461,8 @@ class TelegramClient: # Note that for this to work the phone number must be in your contacts some_id = await client.get_peer_id('+34123456789') """ - return await users.get_entity(**locals()) + @forward_call(users.get_input_entity) async def get_input_entity( self: 'TelegramClient', peer: 'hints.EntityLike') -> '_tl.TypeInputPeer': @@ -3531,8 +3527,8 @@ class TelegramClient: # The same applies to IDs, chats or channels. chat = await client.get_input_entity(-123456789) """ - return await users.get_input_entity(**locals()) + @forward_call(users.get_peer_id) async def get_peer_id( self: 'TelegramClient', peer: 'hints.EntityLike') -> int: @@ -3547,60 +3543,70 @@ class TelegramClient: print(await client.get_peer_id('me')) """ - return await users.get_peer_id(**locals()) # endregion Users # region Private + @forward_call(users._call) async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sleep_threshold=None): - return await users._call(**locals()) + pass + @forward_call(updates._update_loop) async def _update_loop(self: 'TelegramClient'): - return await updates._update_loop(**locals()) + pass + @forward_call(messageparse._parse_message_text) async def _parse_message_text(self: 'TelegramClient', message, parse_mode): - return await messageparse._parse_message_text(**locals()) + pass + @forward_call(uploads._file_to_media) async def _file_to_media( self, file, force_document=False, file_size=None, progress_callback=None, attributes=None, thumb=None, allow_cache=True, voice_note=False, video_note=False, supports_streaming=False, mime_type=None, as_image=None, ttl=None): - return await uploads._file_to_media(**locals()) + pass + @forward_call(messageparse._get_response_message) def _get_response_message(self: 'TelegramClient', request, result, input_chat): - return messageparse._get_response_message(**locals()) + pass + @forward_call(messages._get_comment_data) async def _get_comment_data( self: 'TelegramClient', entity: 'hints.EntityLike', message: 'typing.Union[int, _tl.Message]' ): - return await messages._get_comment_data(**locals()) + pass + @forward_call(telegrambaseclient._switch_dc) async def _switch_dc(self: 'TelegramClient', new_dc): - return await telegrambaseclient._switch_dc(**locals()) + pass + @forward_call(telegrambaseclient._borrow_exported_sender) async def _borrow_exported_sender(self: 'TelegramClient', dc_id): - return await telegrambaseclient._borrow_exported_sender(**locals()) + pass + @forward_call(telegrambaseclient._return_exported_sender) async def _return_exported_sender(self: 'TelegramClient', sender): - return await telegrambaseclient._return_exported_sender(**locals()) + pass + @forward_call(telegrambaseclient._clean_exported_senders) async def _clean_exported_senders(self: 'TelegramClient'): - return await telegrambaseclient._clean_exported_senders(**locals()) + pass + @forward_call(updates._handle_update) def _handle_update(self: 'TelegramClient', update): - return updates._handle_update(**locals()) + pass + @forward_call(updates._handle_auto_reconnect) async def _handle_auto_reconnect(self: 'TelegramClient'): - return await updates._handle_auto_reconnect(**locals()) + pass + @forward_call(auth._update_session_state) async def _update_session_state(self, user, save=True): - return await auth._update_session_state(**locals()) + pass # endregion Private - -# TODO re-patch everything to remove the intermediate calls From 1762f554df897ba000acd16034c102ec54c406ca Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 24 Sep 2021 21:11:50 +0200 Subject: [PATCH 096/256] Make events subpackage private --- telethon/_client/telegramclient.py | 2 +- telethon/_client/updates.py | 16 +++++++++------- telethon/_events/__init__.py | 0 telethon/{events => _events}/album.py | 0 telethon/{events/__init__.py => _events/base.py} | 9 --------- telethon/{events => _events}/callbackquery.py | 0 telethon/{events => _events}/chataction.py | 0 telethon/{events => _events}/common.py | 0 telethon/{events => _events}/inlinequery.py | 0 telethon/{events => _events}/messagedeleted.py | 0 telethon/{events => _events}/messageedited.py | 0 telethon/{events => _events}/messageread.py | 0 telethon/{events => _events}/newmessage.py | 0 telethon/{events => _events}/raw.py | 0 telethon/{events => _events}/userupdate.py | 0 telethon/events.py | 12 ++++++++++++ telethon/types/_custom/qrlogin.py | 5 +++-- 17 files changed, 25 insertions(+), 19 deletions(-) create mode 100644 telethon/_events/__init__.py rename telethon/{events => _events}/album.py (100%) rename telethon/{events/__init__.py => _events/base.py} (92%) rename telethon/{events => _events}/callbackquery.py (100%) rename telethon/{events => _events}/chataction.py (100%) rename telethon/{events => _events}/common.py (100%) rename telethon/{events => _events}/inlinequery.py (100%) rename telethon/{events => _events}/messagedeleted.py (100%) rename telethon/{events => _events}/messageedited.py (100%) rename telethon/{events => _events}/messageread.py (100%) rename telethon/{events => _events}/newmessage.py (100%) rename telethon/{events => _events}/raw.py (100%) rename telethon/{events => _events}/userupdate.py (100%) create mode 100644 telethon/events.py diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 54ab9710..5812296c 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -11,7 +11,7 @@ from . import ( from .. import helpers, version, _tl from ..types import _custom from .._network import ConnectionTcpFull -from ..events.common import EventBuilder, EventCommon +from .._events.common import EventBuilder, EventCommon from .._misc import enums diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index da60b24c..6ff3c9d1 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -8,9 +8,11 @@ import traceback import typing import logging -from .. import events, utils, _tl +from .. import utils, _tl from ..errors._rpcbase import RpcError -from ..events.common import EventBuilder, EventCommon +from .._events.common import EventBuilder, EventCommon +from .._events.raw import Raw +from .._events.base import StopPropagation, _get_handlers if typing.TYPE_CHECKING: from .telegramclient import TelegramClient @@ -40,7 +42,7 @@ def add_event_handler( self: 'TelegramClient', callback: Callback, event: EventBuilder = None): - builders = events._get_handlers(callback) + builders = _get_handlers(callback) if builders is not None: for event in builders: self._event_builders.append((event, callback)) @@ -49,7 +51,7 @@ def add_event_handler( if isinstance(event, type): event = event() elif not event: - event = events.Raw() + event = Raw() self._event_builders.append((event, callback)) @@ -274,7 +276,7 @@ async def _dispatch_update(self: 'TelegramClient', update, others, channel_id, p try: await callback(event) - except events.StopPropagation: + except StopPropagation: name = getattr(callback, '__name__', repr(callback)) self._log[__name__].debug( 'Event handler "%s" stopped chain of propagation ' @@ -294,7 +296,7 @@ async def _dispatch_event(self: 'TelegramClient', event): # the name of speed; we don't want to make it worse for all updates # just because albums may need it. for builder, callback in self._event_builders: - if isinstance(builder, events.Raw): + if isinstance(builder, Raw): continue if not isinstance(event, builder.Event): continue @@ -310,7 +312,7 @@ async def _dispatch_event(self: 'TelegramClient', event): try: await callback(event) - except events.StopPropagation: + except StopPropagation: name = getattr(callback, '__name__', repr(callback)) self._log[__name__].debug( 'Event handler "%s" stopped chain of propagation ' diff --git a/telethon/_events/__init__.py b/telethon/_events/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/telethon/events/album.py b/telethon/_events/album.py similarity index 100% rename from telethon/events/album.py rename to telethon/_events/album.py diff --git a/telethon/events/__init__.py b/telethon/_events/base.py similarity index 92% rename from telethon/events/__init__.py rename to telethon/_events/base.py index 28f85b12..8f913ad7 100644 --- a/telethon/events/__init__.py +++ b/telethon/_events/base.py @@ -1,13 +1,4 @@ from .raw import Raw -from .album import Album -from .chataction import ChatAction -from .messagedeleted import MessageDeleted -from .messageedited import MessageEdited -from .messageread import MessageRead -from .newmessage import NewMessage -from .userupdate import UserUpdate -from .callbackquery import CallbackQuery -from .inlinequery import InlineQuery _HANDLERS_ATTRIBUTE = '__tl.handlers' diff --git a/telethon/events/callbackquery.py b/telethon/_events/callbackquery.py similarity index 100% rename from telethon/events/callbackquery.py rename to telethon/_events/callbackquery.py diff --git a/telethon/events/chataction.py b/telethon/_events/chataction.py similarity index 100% rename from telethon/events/chataction.py rename to telethon/_events/chataction.py diff --git a/telethon/events/common.py b/telethon/_events/common.py similarity index 100% rename from telethon/events/common.py rename to telethon/_events/common.py diff --git a/telethon/events/inlinequery.py b/telethon/_events/inlinequery.py similarity index 100% rename from telethon/events/inlinequery.py rename to telethon/_events/inlinequery.py diff --git a/telethon/events/messagedeleted.py b/telethon/_events/messagedeleted.py similarity index 100% rename from telethon/events/messagedeleted.py rename to telethon/_events/messagedeleted.py diff --git a/telethon/events/messageedited.py b/telethon/_events/messageedited.py similarity index 100% rename from telethon/events/messageedited.py rename to telethon/_events/messageedited.py diff --git a/telethon/events/messageread.py b/telethon/_events/messageread.py similarity index 100% rename from telethon/events/messageread.py rename to telethon/_events/messageread.py diff --git a/telethon/events/newmessage.py b/telethon/_events/newmessage.py similarity index 100% rename from telethon/events/newmessage.py rename to telethon/_events/newmessage.py diff --git a/telethon/events/raw.py b/telethon/_events/raw.py similarity index 100% rename from telethon/events/raw.py rename to telethon/_events/raw.py diff --git a/telethon/events/userupdate.py b/telethon/_events/userupdate.py similarity index 100% rename from telethon/events/userupdate.py rename to telethon/_events/userupdate.py diff --git a/telethon/events.py b/telethon/events.py new file mode 100644 index 00000000..5dca03ae --- /dev/null +++ b/telethon/events.py @@ -0,0 +1,12 @@ +from ._events.base import StopPropagation, register, unregister, is_handler, list +from ._events.raw import Raw + +from ._events.album import Album +from ._events.chataction import ChatAction +from ._events.messagedeleted import MessageDeleted +from ._events.messageedited import MessageEdited +from ._events.messageread import MessageRead +from ._events.newmessage import NewMessage +from ._events.userupdate import UserUpdate +from ._events.callbackquery import CallbackQuery +from ._events.inlinequery import InlineQuery diff --git a/telethon/types/_custom/qrlogin.py b/telethon/types/_custom/qrlogin.py index 9a48884a..473e4bd0 100644 --- a/telethon/types/_custom/qrlogin.py +++ b/telethon/types/_custom/qrlogin.py @@ -2,7 +2,8 @@ import asyncio import base64 import datetime -from ... import events, _tl +from ... import _tl +from ..._events.raw import Raw class QRLogin: @@ -94,7 +95,7 @@ class QRLogin: async def handler(_update): event.set() - self._client.add_event_handler(handler, events.Raw(_tl.UpdateLoginToken)) + self._client.add_event_handler(handler, Raw(_tl.UpdateLoginToken)) try: # Will raise timeout error if it doesn't complete quick enough, From 6fec2a68c5a9953ffedc50a4e10ae1f5eec175ff Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 25 Sep 2021 20:33:25 +0200 Subject: [PATCH 097/256] Use a proper markdown parser --- readthedocs/misc/v2-migration-guide.rst | 21 +++ requirements.txt | 5 +- telethon/_misc/markdown.py | 237 +++++++++++------------- 3 files changed, 130 insertions(+), 133 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index b35e076d..27a4a783 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -206,6 +206,27 @@ The ``telethon.errors`` module continues to provide custom errors used by the li // TODO should RpcError subclass ValueError? technically the values used in the request somehow were wrong… // TODO provide a way to see which errors are known in the docs or at tl.telethon.dev + +The default markdown parse mode now conforms to the commonmark specification +---------------------------------------------------------------------------- + +The old markdown parser (which was used as the default ``client.parse_mode``) used to emulate +Telegram Desktop's behaviour. Now ``__ +is used instead, which fixes certain parsing bugs but also means the formatting will be different. + +Most notably, ``__`` will now make text bold. If you want the old behaviour, use a single +underscore instead (such as ``_``). You can also use a single asterisk (``*``) for italics. +Because now there's proper parsing, you also gain: + +* Headings (``# text``) will now be underlined. +* Certain HTML tags will now also be recognized in markdown (including ```` for underlining text). +* Line breaks behave properly now. For a single-line break, end your line with ``\\``. +* Inline links should no longer behave in a strange manner. +* Pre-blocks can now have a language. Official clients don't syntax highlight code yet, though. + +// TODO provide a way to get back the old behaviour? + + The "iter" variant of the client methods have been removed ---------------------------------------------------------- diff --git a/requirements.txt b/requirements.txt index 2b650ec4..a7d0e620 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ -pyaes -rsa +markdown-it-py~=1.1.0 +pyaes~=1.6.1 +rsa~=4.7.2 diff --git a/telethon/_misc/markdown.py b/telethon/_misc/markdown.py index 77577ee0..8dc82701 100644 --- a/telethon/_misc/markdown.py +++ b/telethon/_misc/markdown.py @@ -5,177 +5,152 @@ since they seem to count as two characters and it's a bit strange. """ import re import warnings +import markdown_it from .helpers import add_surrogate, del_surrogate, within_surrogate, strip_text from .. import _tl from .._misc import tlobject -DEFAULT_DELIMITERS = { - '**': _tl.MessageEntityBold, - '__': _tl.MessageEntityItalic, - '~~': _tl.MessageEntityStrike, - '`': _tl.MessageEntityCode, - '```': _tl.MessageEntityPre + +MARKDOWN = markdown_it.MarkdownIt().enable('strikethrough') +DELIMITERS = { + _tl.MessageEntityBlockquote: ('> ', ''), + _tl.MessageEntityBold: ('**', '**'), + _tl.MessageEntityCode: ('`', '`'), + _tl.MessageEntityItalic: ('_', '_'), + _tl.MessageEntityStrike: ('~~', '~~'), + _tl.MessageEntityUnderline: ('# ', ''), } -DEFAULT_URL_RE = re.compile(r'\[([\S\s]+?)\]\((.+?)\)') -DEFAULT_URL_FORMAT = '[{0}]({1})' +# Not trying to be complete; just enough to have an alternative (mostly for inline underline). +# The fact headings are treated as underline is an implementation detail. +TAG_PATTERN = re.compile(r'<\s*(/?)\s*(\w+)') +HTML_TO_TYPE = { + 'i': ('em_close', 'em_open'), + 'em': ('em_close', 'em_open'), + 'b': ('strong_close', 'strong_open'), + 'strong': ('strong_close', 'strong_open'), + 's': ('s_close', 's_open'), + 'del': ('s_close', 's_open'), + 'u': ('heading_open', 'heading_close'), + 'mark': ('heading_open', 'heading_close'), +} -def overlap(a, b, x, y): - return max(a, x) < min(b, y) +def expand_inline_and_html(tokens): + for token in tokens: + if token.type == 'inline': + yield from expand_inline_and_html(token.children) + elif token.type == 'html_inline': + match = TAG_PATTERN.match(token.content) + if match: + close, tag = match.groups() + tys = HTML_TO_TYPE.get(tag.lower()) + if tys: + token.type = tys[bool(close)] + token.nesting = -1 if close else 1 + yield token + else: + yield token -def parse(message, delimiters=None, url_re=None): +def parse(message): """ Parses the given markdown message and returns its stripped representation plus a list of the _tl.MessageEntity's that were found. - - :param message: the message with markdown-like syntax to be parsed. - :param delimiters: the delimiters to be used, {delimiter: type}. - :param url_re: the URL bytes regex to be used. Must have two groups. - :return: a tuple consisting of (clean message, [message entities]). """ if not message: return message, [] - if url_re is None: - url_re = DEFAULT_URL_RE - elif isinstance(url_re, str): - url_re = re.compile(url_re) + def push(ty, **extra): + nonlocal message, entities, token + if token.nesting > 0: + entities.append(ty(offset=len(message), length=0, **extra)) + else: + for entity in reversed(entities): + if isinstance(entity, ty): + entity.length = len(message) - entity.offset + break - if not delimiters: - if delimiters is not None: - return message, [] - delimiters = DEFAULT_DELIMITERS + parsed = MARKDOWN.parse(add_surrogate(message.strip())) + message = '' + entities = [] + last_map = [0, 0] + for token in expand_inline_and_html(parsed): + if token.map is not None and token.map != last_map: + # paragraphs, quotes fences have a line mapping. Use it to determine how many newlines to insert. + # But don't inssert any (leading) new lines if we're yet to reach the first textual content, or + # if the mappings are the same (e.g. a quote then opens a paragraph but the mapping is equal). + if message: + message += '\n' + '\n' * (token.map[0] - last_map[-1]) + last_map = token.map - # Build a regex to efficiently test all delimiters at once. - # Note that the largest delimiter should go first, we don't - # want ``` to be interpreted as a single back-tick in a code block. - delim_re = re.compile('|'.join('({})'.format(re.escape(k)) - for k in sorted(delimiters, key=len, reverse=True))) + if token.type in ('blockquote_close', 'blockquote_open'): + push(_tl.MessageEntityBlockquote) + elif token.type == 'code_block': + entities.append(_tl.MessageEntityPre(offset=len(message), length=len(token.content), language='')) + message += token.content + elif token.type == 'code_inline': + entities.append(_tl.MessageEntityCode(offset=len(message), length=len(token.content))) + message += token.content + elif token.type in ('em_close', 'em_open'): + push(_tl.MessageEntityItalic) + elif token.type == 'fence': + entities.append(_tl.MessageEntityPre(offset=len(message), length=len(token.content), language=token.info)) + message += token.content[:-1] # remove a single trailing newline + elif token.type == 'hardbreak': + message += '\n' + elif token.type in ('heading_close', 'heading_open'): + push(_tl.MessageEntityUnderline) + elif token.type == 'hr': + message += '\u2015\n\n' + elif token.type in ('link_close', 'link_open'): + if token.markup != 'autolink': # telegram already picks up on these automatically + push(_tl.MessageEntityTextUrl, url=token.attrs.get('href')) + elif token.type in ('s_close', 's_open'): + push(_tl.MessageEntityStrike) + elif token.type == 'softbreak': + message += ' ' + elif token.type in ('strong_close', 'strong_open'): + push(_tl.MessageEntityBold) + elif token.type == 'text': + message += token.content - # Cannot use a for loop because we need to skip some indices - i = 0 - result = [] - - # Work on byte level with the utf-16le encoding to get the offsets right. - # The offset will just be half the index we're at. - message = add_surrogate(message) - while i < len(message): - m = delim_re.match(message, pos=i) - - # Did we find some delimiter here at `i`? - if m: - delim = next(filter(None, m.groups())) - - # +1 to avoid matching right after (e.g. "****") - end = message.find(delim, i + len(delim) + 1) - - # Did we find the earliest closing tag? - if end != -1: - - # Remove the delimiter from the string - message = ''.join(( - message[:i], - message[i + len(delim):end], - message[end + len(delim):] - )) - - # Check other affected entities - for ent in result: - # If the end is after our start, it is affected - if ent.offset + ent.length > i: - # If the old start is also before ours, it is fully enclosed - if ent.offset <= i: - ent.length -= len(delim) * 2 - else: - ent.length -= len(delim) - - # Append the found entity - ent = delimiters[delim] - if ent == _tl.MessageEntityPre: - result.append(ent(i, end - i - len(delim), '')) # has 'lang' - else: - result.append(ent(i, end - i - len(delim))) - - # No nested entities inside code blocks - if ent in (_tl.MessageEntityCode, _tl.MessageEntityPre): - i = end - len(delim) - - continue - - elif url_re: - m = url_re.match(message, pos=i) - if m: - # Replace the whole match with only the inline URL text. - message = ''.join(( - message[:m.start()], - m.group(1), - message[m.end():] - )) - - delim_size = m.end() - m.start() - len(m.group()) - for ent in result: - # If the end is after our start, it is affected - if ent.offset + ent.length > m.start(): - ent.length -= delim_size - - result.append(_tl.MessageEntityTextUrl( - offset=m.start(), length=len(m.group(1)), - url=del_surrogate(m.group(2)) - )) - i += len(m.group(1)) - continue - - i += 1 - - message = strip_text(message, result) - return del_surrogate(message), result + return del_surrogate(message), entities -def unparse(text, entities, delimiters=None, url_fmt=None): +def unparse(text, entities): """ Performs the reverse operation to .parse(), effectively returning markdown-like syntax given a normal text and its _tl.MessageEntity's. - :param text: the text to be reconverted into markdown. - :param entities: the _tl.MessageEntity's applied to the text. - :return: a markdown-like text representing the combination of both inputs. + Because there are many possible ways for markdown to produce a certain + output, this function cannot invert .parse() perfectly. """ if not text or not entities: return text - if not delimiters: - if delimiters is not None: - return text - delimiters = DEFAULT_DELIMITERS - - if url_fmt is not None: - warnings.warn('url_fmt is deprecated') # since it complicates everything *a lot* - if isinstance(entities, tlobject.TLObject): entities = (entities,) text = add_surrogate(text) - delimiters = {v: k for k, v in delimiters.items()} insert_at = [] for entity in entities: s = entity.offset e = entity.offset + entity.length - delimiter = delimiters.get(type(entity), None) + delimiter = DELIMITERS.get(type(entity), None) if delimiter: - insert_at.append((s, delimiter)) - insert_at.append((e, delimiter)) - else: - url = None - if isinstance(entity, _tl.MessageEntityTextUrl): - url = entity.url - elif isinstance(entity, _tl.MessageEntityMentionName): - url = 'tg://user?id={}'.format(entity.user_id) - if url: - insert_at.append((s, '[')) - insert_at.append((e, ']({})'.format(url))) + insert_at.append((s, delimiter[0])) + insert_at.append((e, delimiter[1])) + elif isinstance(entity, _tl.MessageEntityPre): + insert_at.append((s, f'```{entity.language}\n')) + insert_at.append((e, '```\n')) + elif isinstance(entity, _tl.MessageEntityTextUrl): + insert_at.append((s, '[')) + insert_at.append((e, f']({entity.url})')) + elif isinstance(entity, _tl.MessageEntityMentionName): + insert_at.append((s, '[')) + insert_at.append((e, f'](tg://user?id={entity.user_id})')) insert_at.sort(key=lambda t: t[0]) while insert_at: From 8bd4835eb21943245ef3d9479d7769957cd0a40c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 25 Sep 2021 20:42:51 +0200 Subject: [PATCH 098/256] Remove build_reply_markup from the client --- readthedocs/misc/v2-migration-guide.rst | 11 ++-- telethon/_client/buttons.py | 66 -------------------- telethon/_client/messages.py | 8 +-- telethon/_client/telegramclient.py | 42 +------------ telethon/_client/uploads.py | 2 +- telethon/types/_custom/button.py | 82 +++++++++++++++++++++++++ telethon/types/_custom/inlinebuilder.py | 3 +- 7 files changed, 97 insertions(+), 117 deletions(-) delete mode 100644 telethon/_client/buttons.py diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 27a4a783..e3f9be6a 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -386,14 +386,17 @@ Note that you do not need to ``await`` the call to ``.start()`` if you are going in a context-manager (but it's okay if you put the ``await``). -download_file has been removed from the client ----------------------------------------------- - -Instead, ``client.download_media`` should be used. +Several methods have been removed from the client +------------------------------------------------- +``client.download_file`` has been removed. Instead, ``client.download_media`` should be used. The now-removed ``client.download_file`` method was a lower level implementation which should have not been exposed at all. +``client.build_reply_markup`` has been removed. Manually calling this method was purely an +optimization (the buttons won't need to be transformed into a reply markup every time they're +used). This means you can just remove any calls to this method and things will continue to work. + Support for bot-API style file_id has been removed -------------------------------------------------- diff --git a/telethon/_client/buttons.py b/telethon/_client/buttons.py deleted file mode 100644 index 599ebc96..00000000 --- a/telethon/_client/buttons.py +++ /dev/null @@ -1,66 +0,0 @@ -import typing - -from .._misc import utils, hints -from .. import _tl -from ..types import _custom - - -def build_reply_markup( - buttons: 'typing.Optional[hints.MarkupLike]', - inline_only: bool = False) -> 'typing.Optional[_tl.TypeReplyMarkup]': - if not buttons: - return None - - try: - if buttons.SUBCLASS_OF_ID == 0xe2e10ef2: - return buttons # crc32(b'ReplyMarkup'): - except AttributeError: - pass - - if not utils.is_list_like(buttons): - buttons = [buttons] - if not utils.is_list_like(buttons[0]): - buttons = [[b] for b in 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(_tl.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 _tl.ReplyInlineMarkup(rows) - # elif is_normal: - return _tl.ReplyKeyboardMarkup( - rows, resize=resize, single_use=single_use, selective=selective) diff --git a/telethon/_client/messages.py b/telethon/_client/messages.py index 5b7ca110..66d3512e 100644 --- a/telethon/_client/messages.py +++ b/telethon/_client/messages.py @@ -430,7 +430,7 @@ async def send_message( if buttons is None: markup = message.reply_markup else: - markup = self.build_reply_markup(buttons) + markup = _custom.button.build_reply_markup(buttons) if silent is None: silent = message.silent @@ -480,7 +480,7 @@ async def send_message( clear_draft=clear_draft, silent=silent, background=background, - reply_markup=self.build_reply_markup(buttons), + reply_markup=_custom.button.build_reply_markup(buttons), schedule_date=schedule ) @@ -593,7 +593,7 @@ async def edit_message( no_webpage=not link_preview, entities=formatting_entities, media=media, - reply_markup=self.build_reply_markup(buttons) + reply_markup=_custom.button.build_reply_markup(buttons) ) # Invoke `messages.editInlineBotMessage` from the right datacenter. # Otherwise, Telegram will error with `MESSAGE_ID_INVALID` and do nothing. @@ -615,7 +615,7 @@ async def edit_message( no_webpage=not link_preview, entities=formatting_entities, media=media, - reply_markup=self.build_reply_markup(buttons), + reply_markup=_custom.button.build_reply_markup(buttons), schedule_date=schedule ) msg = self._get_response_message(request, await self(request), entity) diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 5812296c..1e23440a 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -5,7 +5,7 @@ import typing import logging from . import ( - account, auth, bots, buttons, chats, dialogs, downloads, messageparse, messages, + account, auth, bots, chats, dialogs, downloads, messageparse, messages, telegrambaseclient, updates, uploads, users ) from .. import helpers, version, _tl @@ -684,46 +684,6 @@ class TelegramClient: # endregion Bots - # region Buttons - - @staticmethod - def build_reply_markup( - buttons: 'typing.Optional[hints.MarkupLike]', - inline_only: bool = False) -> 'typing.Optional[_tl.TypeReplyMarkup]': - """ - Builds a :tl:`ReplyInlineMarkup` or :tl:`ReplyKeyboardMarkup` for - the given buttons. - - Does nothing if either no buttons are provided or the provided - argument is already a reply markup. - - You should consider using this method if you are going to reuse - the markup very often. Otherwise, it is not necessary. - - This method is **not** asynchronous (don't use ``await`` on it). - - Arguments - buttons (`hints.MarkupLike`): - The button, list of buttons, array of buttons or markup - to convert into a markup. - - inline_only (`bool`, optional): - Whether the buttons **must** be inline buttons only or not. - - Example - .. code-block:: python - - from telethon import Button - - markup = client.build_reply_markup(Button.inline('hi')) - # later - await client.send_message(chat, 'click me', buttons=markup) - """ - from . import buttons as b - return b.build_reply_markup(buttons=buttons, inline_only=inline_only) - - # endregion Buttons - # region Chats @forward_call(chats.get_participants) diff --git a/telethon/_client/uploads.py b/telethon/_client/uploads.py index 912954b0..a141d199 100644 --- a/telethon/_client/uploads.py +++ b/telethon/_client/uploads.py @@ -182,7 +182,7 @@ async def send_file( if not media: raise TypeError('Cannot use {!r} as file'.format(file)) - markup = self.build_reply_markup(buttons) + markup = _custom.button.build_reply_markup(buttons) request = _tl.fn.messages.SendMedia( entity, media, reply_to_msg_id=reply_to, message=caption, entities=msg_entities, reply_markup=markup, silent=silent, diff --git a/telethon/types/_custom/button.py b/telethon/types/_custom/button.py index 4dbcab99..ffac7c99 100644 --- a/telethon/types/_custom/button.py +++ b/telethon/types/_custom/button.py @@ -1,3 +1,4 @@ +from .messagebutton import MessageButton from ... import _tl from ..._misc import utils @@ -306,3 +307,84 @@ class Button: documentation for more information on using games. """ return _tl.KeyboardButtonGame(text) + + +def build_reply_markup( + buttons: 'typing.Optional[hints.MarkupLike]', + inline_only: bool = False) -> 'typing.Optional[_tl.TypeReplyMarkup]': + """ + Builds a :tl:`ReplyInlineMarkup` or :tl:`ReplyKeyboardMarkup` for + the given buttons. + + Does nothing if either no buttons are provided or the provided + argument is already a reply markup. + + You should consider using this method if you are going to reuse + the markup very often. Otherwise, it is not necessary. + + This method is **not** asynchronous (don't use ``await`` on it). + + Arguments + buttons (`hints.MarkupLike`): + The button, list of buttons, array of buttons or markup + to convert into a markup. + + inline_only (`bool`, optional): + Whether the buttons **must** be inline buttons only or not. + """ + if not buttons: + return None + + try: + if buttons.SUBCLASS_OF_ID == 0xe2e10ef2: + return buttons # crc32(b'ReplyMarkup'): + except AttributeError: + pass + + if not utils.is_list_like(buttons): + buttons = [buttons] + if not utils.is_list_like(buttons[0]): + buttons = [[b] for b in 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, 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, MessageButton): + button = button.button + + inline = 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(_tl.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 _tl.ReplyInlineMarkup(rows) + # elif is_normal: + return _tl.ReplyKeyboardMarkup( + rows, resize=resize, single_use=single_use, selective=selective) diff --git a/telethon/types/_custom/inlinebuilder.py b/telethon/types/_custom/inlinebuilder.py index b401ab04..3fea2fc2 100644 --- a/telethon/types/_custom/inlinebuilder.py +++ b/telethon/types/_custom/inlinebuilder.py @@ -2,6 +2,7 @@ import hashlib from ... import _tl from ..._misc import utils +from ...types import _custom _TYPE_TO_MIMES = { 'gif': ['image/gif'], # 'video/mp4' too, but that's used for video @@ -391,7 +392,7 @@ class InlineBuilder: 'text geo contact game'.split(), args) if x[1]) or 'none') ) - markup = self._client.build_reply_markup(buttons, inline_only=True) + markup = _custom.button.build_reply_markup(buttons, inline_only=True) if text is not None: text, msg_entities = await self._client._parse_message_text( text, parse_mode From 86c47a277113ec325ef6336ed925e72a8a1b4588 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 26 Sep 2021 17:52:16 +0200 Subject: [PATCH 099/256] Use __slots__ in all generated classes --- readthedocs/misc/v2-migration-guide.rst | 11 ++++++-- telethon/_client/updates.py | 34 +++++++++++------------ telethon/_misc/tlobject.py | 3 +- telethon/types/_custom/message.py | 7 ++--- telethon_generator/generators/tlobject.py | 9 ++++++ 5 files changed, 38 insertions(+), 26 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index e3f9be6a..3a3f18cf 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -286,8 +286,8 @@ results into a list: // TODO does the download really need to be special? get download is kind of weird though -Raw API methods have been renamed and are now considered private ----------------------------------------------------------------- +Raw API has been renamed and is now considered private +------------------------------------------------------ The subpackage holding the raw API methods has been renamed from ``tl`` to ``_tl`` in order to signal that these are prone to change across minor version bumps (the ``y`` in version ``x.y.z``). @@ -324,7 +324,14 @@ This serves multiple goals: identify which parts are making use of it. * The name is shorter, but remains recognizable. +Because *a lot* of these objects are created, they now define ``__slots__``. This means you can +no longer monkey-patch them to add new attributes at runtime. You have to create a subclass if you +want to define new attributes. + +This also means that the updates from ``events.Raw`` **no longer have** ``update._entities``. + // TODO this definitely generated files mapping from the original name to this new one... +// TODO what's the alternative to update._entities? and update._client?? Many subpackages and modules are now private diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index 6ff3c9d1..e40ff9fa 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -143,22 +143,20 @@ def _handle_update(self: 'TelegramClient', update): entities = {utils.get_peer_id(x): x for x in itertools.chain(update.users, update.chats)} for u in update.updates: - _process_update(self, u, update.updates, entities=entities) + _process_update(self, u, entities, update.updates) elif isinstance(update, _tl.UpdateShort): - _process_update(self, update.update, None) + _process_update(self, update.update, {}, None) else: - _process_update(self, update, None) + _process_update(self, update, {}, None) self._state_cache.update(update) -def _process_update(self: 'TelegramClient', update, others, entities=None): - update._entities = entities or {} - +def _process_update(self: 'TelegramClient', update, entities, others): # This part is somewhat hot so we don't bother patching # update with channel ID/its state. Instead we just pass # arguments which is faster. channel_id = self._state_cache.get_channel_id(update) - args = (update, others, channel_id, self._state_cache[channel_id]) + args = (update, entities, others, channel_id, self._state_cache[channel_id]) if self._dispatching_updates_queue is None: task = self.loop.create_task(_dispatch_update(self, *args)) self._updates_queue.add(task) @@ -231,10 +229,11 @@ async def _dispatch_queue_updates(self: 'TelegramClient'): self._dispatching_updates_queue.clear() -async def _dispatch_update(self: 'TelegramClient', update, others, channel_id, pts_date): - entities = self._entity_cache.add(list((update._entities or {}).values())) +async def _dispatch_update(self: 'TelegramClient', update, entities, others, channel_id, pts_date): if entities: - await self.session.insert_entities(entities) + rows = self._entity_cache.add(list(entities.values())) + if rows: + await self.session.insert_entities(rows) if not self._entity_cache.ensure_cached(update): # We could add a lock to not fetch the same pts twice if we are @@ -244,7 +243,7 @@ async def _dispatch_update(self: 'TelegramClient', update, others, channel_id, p # If the update doesn't have pts, fetching won't do anything. # For example, UpdateUserStatus or UpdateChatUserTyping. try: - await _get_difference(self, update, channel_id, pts_date) + await _get_difference(self, update, entities, channel_id, pts_date) except OSError: pass # We were disconnected, that's okay except RpcError: @@ -258,7 +257,7 @@ async def _dispatch_update(self: 'TelegramClient', update, others, channel_id, p # ValueError("Request was unsuccessful N time(s)") for whatever reasons. pass - built = EventBuilderDict(self, update, others) + built = EventBuilderDict(self, update, entities, others) for builder, callback in self._event_builders: event = built[type(builder)] @@ -324,7 +323,7 @@ async def _dispatch_event(self: 'TelegramClient', event): name = getattr(callback, '__name__', repr(callback)) self._log[__name__].exception('Unhandled exception on %s', name) -async def _get_difference(self: 'TelegramClient', update, channel_id, pts_date): +async def _get_difference(self: 'TelegramClient', update, entities, channel_id, pts_date): """ Get the difference for this `channel_id` if any, then load entities. @@ -380,7 +379,7 @@ async def _get_difference(self: 'TelegramClient', update, channel_id, pts_date): _tl.updates.DifferenceSlice, _tl.updates.ChannelDifference, _tl.updates.ChannelDifferenceTooLong)): - update._entities.update({ + entities.update({ utils.get_peer_id(x): x for x in itertools.chain(result.users, result.chats) }) @@ -433,9 +432,10 @@ class EventBuilderDict: """ Helper "dictionary" to return events from types and cache them. """ - def __init__(self, client: 'TelegramClient', update, others): + def __init__(self, client: 'TelegramClient', update, entities, others): self.client = client self.update = update + self.entities = entities self.others = others def __getitem__(self, builder): @@ -447,9 +447,7 @@ class EventBuilderDict: if isinstance(event, EventCommon): event.original_update = self.update - event._entities = self.update._entities + event._entities = self.entities or {} event._set_client(self.client) - elif event: - event._client = self.client return event diff --git a/telethon/_misc/tlobject.py b/telethon/_misc/tlobject.py index 4b94e00f..c9e3d425 100644 --- a/telethon/_misc/tlobject.py +++ b/telethon/_misc/tlobject.py @@ -13,7 +13,7 @@ def _datetime_to_timestamp(dt): # If no timezone is specified, it is assumed to be in utc zone if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) - # We use .total_seconds() method instead of simply dt.timestamp(), + # We use .total_seconds() method instead of simply dt.timestamp(), # because on Windows the latter raises OSError on datetimes ~< datetime(1970,1,1) secs = int((dt - _EPOCH).total_seconds()) # Make sure it's a valid signed 32 bit integer, as used by Telegram. @@ -32,6 +32,7 @@ def _json_default(value): class TLObject: + __slots__ = () CONSTRUCTOR_ID = None SUBCLASS_OF_ID = None diff --git a/telethon/types/_custom/message.py b/telethon/types/_custom/message.py index 4d5d615a..ef7cf734 100644 --- a/telethon/types/_custom/message.py +++ b/telethon/types/_custom/message.py @@ -11,13 +11,10 @@ from ... import _tl def _fwd(field, doc): def fget(self): - try: - return self._message.__dict__[field] - except KeyError: - return None + return getattr(self._message, field, None) def fset(self, value): - self._message.__dict__[field] = value + setattr(self._message, field, value) return property(fget, fset, None, doc) diff --git a/telethon_generator/generators/tlobject.py b/telethon_generator/generators/tlobject.py index bb310c5a..55c580b0 100644 --- a/telethon_generator/generators/tlobject.py +++ b/telethon_generator/generators/tlobject.py @@ -189,6 +189,15 @@ def _write_class_init(tlobject, kind, type_constructors, builder): builder.writeln() builder.writeln('class {}({}):', tlobject.class_name, kind) + # Define slots to help reduce the size of the objects a little bit. + # It's also good for knowing what fields an object has. + builder.write('__slots__ = (') + sep = '' + for arg in tlobject.real_args: + builder.write('{}{!r},', sep, arg.name) + sep = ' ' + builder.writeln(')') + # Class-level variable to store its Telegram's constructor ID builder.writeln('CONSTRUCTOR_ID = {:#x}', tlobject.id) builder.writeln('SUBCLASS_OF_ID = {:#x}', From e3b1dc205952091981a0a36d9213d2aa672816ac Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 26 Sep 2021 18:30:08 +0200 Subject: [PATCH 100/256] Make to_dict dynamic --- readthedocs/misc/v2-migration-guide.rst | 11 +++++++ telethon/_misc/tlobject.py | 17 ++++++++++- telethon_generator/generators/tlobject.py | 37 ----------------------- 3 files changed, 27 insertions(+), 38 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 3a3f18cf..61ccf252 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -330,6 +330,17 @@ want to define new attributes. This also means that the updates from ``events.Raw`` **no longer have** ``update._entities``. +``tlobject.to_dict()`` has changed and is now generated dynamically based on the ``__slots__`. +This may incur a small performance hit (but you shouldn't really be using ``.to_dict()`` when +you can just use attribute access and ``getattr``). In general, this should handle ill-defined +objects more gracefully (for instance, those where you're using a ``tuple`` and not a ``list`` +or using a list somewhere it shouldn't be), and have no other observable effects. As an extra +benefit, this slightly cuts down on the amount of bloat. + +In ``tlobject.to_dict()``, the special ``_`` key is now also contains the module (so you can +actually distinguish between equally-named classes). If you want the old behaviour, use +``tlobject.__class__.__name__` instead (and add ``Request`` for functions). + // TODO this definitely generated files mapping from the original name to this new one... // TODO what's the alternative to update._entities? and update._client?? diff --git a/telethon/_misc/tlobject.py b/telethon/_misc/tlobject.py index c9e3d425..da2ed6d6 100644 --- a/telethon/_misc/tlobject.py +++ b/telethon/_misc/tlobject.py @@ -171,7 +171,22 @@ class TLObject: return TLObject.pretty_format(self, indent=0) def to_dict(self): - raise NotImplementedError + res = {} + pre = ('', 'fn.')[isinstance(self, TLRequest)] + mod = self.__class__.__module__[self.__class__.__module__.rfind('.') + 1:] + if mod in ('_tl', 'fn'): + res['_'] = f'{pre}{self.__class__.__name__}' + else: + res['_'] = f'{pre}{mod}.{self.__class__.__name__}' + + for slot in self.__slots__: + attr = getattr(self, slot) + if isinstance(attr, list): + res[slot] = [val.to_dict() if hasattr(val, 'to_dict') else val for val in attr] + else: + res[slot] = attr.to_dict() if hasattr(attr, 'to_dict') else attr + + return res def to_json(self, fp=None, default=_json_default, **kwargs): """ diff --git a/telethon_generator/generators/tlobject.py b/telethon_generator/generators/tlobject.py index 55c580b0..e5a3d07e 100644 --- a/telethon_generator/generators/tlobject.py +++ b/telethon_generator/generators/tlobject.py @@ -178,7 +178,6 @@ def _write_source_code(tlobject, kind, builder, type_constructors): """ _write_class_init(tlobject, kind, type_constructors, builder) _write_resolve(tlobject, builder) - _write_to_dict(tlobject, builder) _write_to_bytes(tlobject, builder) _write_from_reader(tlobject, builder) _write_read_result(tlobject, builder) @@ -301,42 +300,6 @@ def _write_resolve(tlobject, builder): builder.end_block() -def _write_to_dict(tlobject, builder): - builder.writeln('def to_dict(self):') - builder.writeln('return {') - builder.current_indent += 1 - - builder.write("'_': '{}'", tlobject.class_name) - for arg in tlobject.real_args: - builder.writeln(',') - builder.write("'{}': ", arg.name) - if arg.type in BASE_TYPES: - if arg.is_vector: - builder.write('[] if self.{0} is None else self.{0}[:]', - arg.name) - else: - builder.write('self.{}', arg.name) - else: - if arg.is_vector: - builder.write( - '[] if self.{0} is None else [x.to_dict() ' - 'if isinstance(x, TLObject) else x for x in self.{0}]', - arg.name - ) - else: - builder.write( - 'self.{0}.to_dict() ' - 'if isinstance(self.{0}, TLObject) else self.{0}', - arg.name - ) - - builder.writeln() - builder.current_indent -= 1 - builder.writeln("}") - - builder.end_block() - - def _write_to_bytes(tlobject, builder): builder.writeln('def _bytes(self):') From 6f602a203ed675cf50ac67a1ede85e578b086df4 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 26 Sep 2021 18:33:03 +0200 Subject: [PATCH 101/256] Fix custom.Forward not using the new __slots__ --- telethon/types/_custom/forward.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telethon/types/_custom/forward.py b/telethon/types/_custom/forward.py index d6a46cb7..c5839c4b 100644 --- a/telethon/types/_custom/forward.py +++ b/telethon/types/_custom/forward.py @@ -26,7 +26,8 @@ class Forward(ChatGetter, SenderGetter): # Copy all the fields, not reference! It would cause memory cycles: # self.original_fwd.original_fwd.original_fwd.original_fwd # ...would be valid if we referenced. - self.__dict__.update(original.__dict__) + for slot in original.__slots__: + setattr(self, slot, getattr(original, slot)) self.original_fwd = original sender_id = sender = input_sender = peer = chat = input_chat = None From a9e1a574aee050e4b48d32ff35fd52f6242a9c60 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 26 Sep 2021 18:37:09 +0200 Subject: [PATCH 102/256] Fix limit was no longer defaulting to empty tuple Introduced by 5a44510e2dc65bc7829a83290f6a28bfca9105fd. When forwarding the calls, both signantures should match. --- telethon/_client/chats.py | 6 +++--- telethon/_client/dialogs.py | 2 +- telethon/_client/downloads.py | 2 +- telethon/_client/messages.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/telethon/_client/chats.py b/telethon/_client/chats.py index 205113d6..221a420a 100644 --- a/telethon/_client/chats.py +++ b/telethon/_client/chats.py @@ -374,7 +374,7 @@ class _ProfilePhotoIter(requestiter.RequestIter): def get_participants( self: 'TelegramClient', entity: 'hints.EntityLike', - limit: float = None, + limit: float = (), *, search: str = '', filter: '_tl.TypeChannelParticipantsFilter' = None) -> _ParticipantsIter: @@ -390,7 +390,7 @@ def get_participants( def get_admin_log( self: 'TelegramClient', entity: 'hints.EntityLike', - limit: float = None, + limit: float = (), *, max_id: int = 0, min_id: int = 0, @@ -440,7 +440,7 @@ def get_admin_log( def get_profile_photos( self: 'TelegramClient', entity: 'hints.EntityLike', - limit: int = None, + limit: int = (), *, offset: int = 0, max_id: int = 0) -> _ProfilePhotoIter: diff --git a/telethon/_client/dialogs.py b/telethon/_client/dialogs.py index 444a0570..2ee272fe 100644 --- a/telethon/_client/dialogs.py +++ b/telethon/_client/dialogs.py @@ -138,7 +138,7 @@ class _DraftsIter(requestiter.RequestIter): def get_dialogs( self: 'TelegramClient', - limit: float = None, + limit: float = (), *, offset_date: 'hints.DateLike' = None, offset_id: int = 0, diff --git a/telethon/_client/downloads.py b/telethon/_client/downloads.py index 50a383dd..1f340982 100644 --- a/telethon/_client/downloads.py +++ b/telethon/_client/downloads.py @@ -407,7 +407,7 @@ def iter_download( *, offset: int = 0, stride: int = None, - limit: int = None, + limit: int = (), chunk_size: int = None, request_size: int = MAX_CHUNK_SIZE, file_size: int = None, diff --git a/telethon/_client/messages.py b/telethon/_client/messages.py index 66d3512e..ad4324d7 100644 --- a/telethon/_client/messages.py +++ b/telethon/_client/messages.py @@ -329,7 +329,7 @@ async def _get_peer(self: 'TelegramClient', input_peer: 'hints.EntityLike'): def get_messages( self: 'TelegramClient', entity: 'hints.EntityLike', - limit: float = None, + limit: float = (), *, offset_date: 'hints.DateLike' = None, offset_id: int = 0, From 197a1ca996f0223833530d3735c95ca767e4f64d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 26 Sep 2021 19:58:42 +0200 Subject: [PATCH 103/256] Fix some modules were public when they should not have been --- readthedocs/misc/v2-migration-guide.rst | 3 +++ telethon/__init__.py | 8 ++++---- telethon/_client/account.py | 3 ++- telethon/_client/bots.py | 3 ++- telethon/_client/chats.py | 4 ++-- telethon/_client/dialogs.py | 4 ++-- telethon/_client/downloads.py | 4 ++-- telethon/_client/messageparse.py | 3 ++- telethon/_client/messages.py | 4 ++-- telethon/_client/telegrambaseclient.py | 4 ++-- telethon/_client/telegramclient.py | 2 +- telethon/_client/updates.py | 3 ++- telethon/_client/uploads.py | 4 ++-- telethon/_client/users.py | 4 ++-- telethon/_misc/html.py | 3 ++- telethon/_network/authenticator.py | 3 ++- telethon/_network/connection/connection.py | 2 +- telethon/_network/mtprotosender.py | 8 ++++---- telethon/types/_custom/chatgetter.py | 3 ++- 19 files changed, 41 insertions(+), 31 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 61ccf252..9444b87c 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -341,6 +341,9 @@ In ``tlobject.to_dict()``, the special ``_`` key is now also contains the module actually distinguish between equally-named classes). If you want the old behaviour, use ``tlobject.__class__.__name__` instead (and add ``Request`` for functions). +Because the string representation of an object used ``tlobject.to_dict()``, it is now also +affected by these changes. + // TODO this definitely generated files mapping from the original name to this new one... // TODO what's the alternative to update._entities? and update._client?? diff --git a/telethon/__init__.py b/telethon/__init__.py index a10dc90c..86e0580f 100644 --- a/telethon/__init__.py +++ b/telethon/__init__.py @@ -1,10 +1,10 @@ # Note: the import order matters -from ._misc import helpers # no dependencies +from ._misc import helpers as _ # no dependencies from . import _tl # no dependencies -from ._misc import utils # depends on helpers and _tl -from ._misc import hints # depends on types/custom +from ._misc import utils as _ # depends on helpers and _tl +from ._misc import hints as _ # depends on types/custom from ._client.telegramclient import TelegramClient -from . import version, events, utils, errors, enums +from . import version, events, errors, enums __version__ = version.__version__ diff --git a/telethon/_client/account.py b/telethon/_client/account.py index 0331a195..eedad595 100644 --- a/telethon/_client/account.py +++ b/telethon/_client/account.py @@ -3,7 +3,8 @@ import inspect import typing from .users import _NOT_A_REQUEST -from .. import helpers, utils, _tl +from .._misc import helpers, utils +from .. import _tl if typing.TYPE_CHECKING: from .telegramclient import TelegramClient diff --git a/telethon/_client/bots.py b/telethon/_client/bots.py index 9360d7f3..2e3ef1b6 100644 --- a/telethon/_client/bots.py +++ b/telethon/_client/bots.py @@ -1,7 +1,8 @@ import typing -from .. import hints, _tl from ..types import _custom +from .._misc import hints +from .. import _tl if typing.TYPE_CHECKING: from .telegramclient import TelegramClient diff --git a/telethon/_client/chats.py b/telethon/_client/chats.py index 221a420a..ffb68c0f 100644 --- a/telethon/_client/chats.py +++ b/telethon/_client/chats.py @@ -4,8 +4,8 @@ import itertools import string import typing -from .. import hints, errors, _tl -from .._misc import helpers, utils, requestiter, tlobject, enums +from .. import errors, _tl +from .._misc import helpers, utils, requestiter, tlobject, enums, hints from ..types import _custom if typing.TYPE_CHECKING: diff --git a/telethon/_client/dialogs.py b/telethon/_client/dialogs.py index 2ee272fe..e3832ee8 100644 --- a/telethon/_client/dialogs.py +++ b/telethon/_client/dialogs.py @@ -3,8 +3,8 @@ import inspect import itertools import typing -from .. import hints, errors, _tl -from .._misc import helpers, utils, requestiter +from .. import errors, _tl +from .._misc import helpers, utils, requestiter, hints from ..types import _custom _MAX_CHUNK_SIZE = 100 diff --git a/telethon/_client/downloads.py b/telethon/_client/downloads.py index 1f340982..b29206d4 100644 --- a/telethon/_client/downloads.py +++ b/telethon/_client/downloads.py @@ -7,8 +7,8 @@ import inspect import asyncio from .._crypto import AES -from .._misc import utils, helpers, requestiter, tlobject -from .. import errors, hints, _tl +from .._misc import utils, helpers, requestiter, tlobject, hints +from .. import errors, _tl try: import aiohttp diff --git a/telethon/_client/messageparse.py b/telethon/_client/messageparse.py index 4cc17e15..b7e76e8d 100644 --- a/telethon/_client/messageparse.py +++ b/telethon/_client/messageparse.py @@ -2,8 +2,9 @@ import itertools import re import typing -from .. import helpers, utils, _tl +from .._misc import helpers, utils from ..types import _custom +from .. import _tl if typing.TYPE_CHECKING: from .telegramclient import TelegramClient diff --git a/telethon/_client/messages.py b/telethon/_client/messages.py index ad4324d7..fd3fbea8 100644 --- a/telethon/_client/messages.py +++ b/telethon/_client/messages.py @@ -3,9 +3,9 @@ import itertools import typing import warnings -from .. import errors, hints, _tl -from .._misc import helpers, utils, requestiter +from .._misc import helpers, utils, requestiter, hints from ..types import _custom +from .. import errors, _tl _MAX_CHUNK_SIZE = 100 diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index a4349c19..9316668b 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -8,9 +8,9 @@ import time import typing import ipaddress -from .. import version, helpers, __name__ as __base_name__, _tl +from .. import version, __name__ as __base_name__, _tl from .._crypto import rsa -from .._misc import markdown, entitycache, statecache, enums +from .._misc import markdown, entitycache, statecache, enums, helpers from .._network import MTProtoSender, Connection, ConnectionTcpFull, connection as conns from .._sessions import Session, SQLiteSession, MemorySession from .._sessions.types import DataCenter, SessionState diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 1e23440a..136718ab 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -8,7 +8,7 @@ from . import ( account, auth, bots, chats, dialogs, downloads, messageparse, messages, telegrambaseclient, updates, uploads, users ) -from .. import helpers, version, _tl +from .. import version, _tl from ..types import _custom from .._network import ConnectionTcpFull from .._events.common import EventBuilder, EventCommon diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index e40ff9fa..71a3e7e8 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -8,11 +8,12 @@ import traceback import typing import logging -from .. import utils, _tl from ..errors._rpcbase import RpcError from .._events.common import EventBuilder, EventCommon from .._events.raw import Raw from .._events.base import StopPropagation, _get_handlers +from .._misc import utils +from .. import _tl if typing.TYPE_CHECKING: from .telegramclient import TelegramClient diff --git a/telethon/_client/uploads.py b/telethon/_client/uploads.py index a141d199..9c203008 100644 --- a/telethon/_client/uploads.py +++ b/telethon/_client/uploads.py @@ -9,9 +9,9 @@ from io import BytesIO from .._crypto import AES -from .._misc import utils, helpers -from .. import hints, _tl +from .._misc import utils, helpers, hints from ..types import _custom +from .. import _tl try: import PIL diff --git a/telethon/_client/users.py b/telethon/_client/users.py index de8a49a3..02dccf95 100644 --- a/telethon/_client/users.py +++ b/telethon/_client/users.py @@ -6,9 +6,9 @@ import typing from ..errors._custom import MultiError from ..errors._rpcbase import RpcError, ServerError, FloodError, InvalidDcError, UnauthorizedError -from .. import errors, hints, _tl -from .._misc import helpers, utils +from .._misc import helpers, utils, hints from .._sessions.types import Entity +from .. import errors, _tl _NOT_A_REQUEST = lambda: TypeError('You can only invoke requests, not types!') diff --git a/telethon/_misc/html.py b/telethon/_misc/html.py index fbf97011..c17f6f7c 100644 --- a/telethon/_misc/html.py +++ b/telethon/_misc/html.py @@ -7,7 +7,8 @@ from html import escape from html.parser import HTMLParser from typing import Iterable, Optional, Tuple, List -from .. import helpers, _tl +from .._misc import helpers +from .. import _tl # Helpers from markdown.py diff --git a/telethon/_network/authenticator.py b/telethon/_network/authenticator.py index 533eeb5c..dfd16469 100644 --- a/telethon/_network/authenticator.py +++ b/telethon/_network/authenticator.py @@ -6,7 +6,8 @@ import os import time from hashlib import sha1 -from .. import helpers, _tl +from .. import _tl +from .._misc import helpers from .._crypto import AES, AuthKey, Factorization, rsa from ..errors._custom import SecurityError from .._misc.binaryreader import BinaryReader diff --git a/telethon/_network/connection/connection.py b/telethon/_network/connection/connection.py index d206b185..4570180d 100644 --- a/telethon/_network/connection/connection.py +++ b/telethon/_network/connection/connection.py @@ -14,7 +14,7 @@ except ImportError: python_socks = None from ...errors._custom import InvalidChecksumError -from ... import helpers +from ..._misc import helpers class Connection(abc.ABC): diff --git a/telethon/_network/mtprotosender.py b/telethon/_network/mtprotosender.py index 20e68d72..29371bf6 100644 --- a/telethon/_network/mtprotosender.py +++ b/telethon/_network/mtprotosender.py @@ -8,7 +8,6 @@ from ..errors._rpcbase import _mk_error_type from .mtprotoplainsender import MTProtoPlainSender from .requeststate import RequestState from .mtprotostate import MTProtoState -from .. import helpers, utils, _tl from ..errors import ( BadMessageError, InvalidBufferError, SecurityError, TypeNotFoundError, rpc_message_to_error @@ -17,7 +16,8 @@ from .._misc.binaryreader import BinaryReader from .._misc.tlobject import TLRequest from ..types._core import RpcResult, MessageContainer, GzipPacked from .._crypto import AuthKey -from .._misc.helpers import retry_range +from .._misc import helpers, utils +from .. import _tl class MTProtoSender: @@ -214,7 +214,7 @@ class MTProtoSender: connected = False - for attempt in retry_range(self._retries): + for attempt in helpers.retry_range(self._retries): if not connected: connected = await self._try_connect(attempt) if not connected: @@ -351,7 +351,7 @@ class MTProtoSender: attempt = 0 ok = True # We're already "retrying" to connect, so we don't want to force retries - for attempt in retry_range(retries, force_retry=False): + for attempt in helpers.retry_range(retries, force_retry=False): try: await self._connect() except (IOError, asyncio.TimeoutError) as e: diff --git a/telethon/types/_custom/chatgetter.py b/telethon/types/_custom/chatgetter.py index d995dce6..13e234ee 100644 --- a/telethon/types/_custom/chatgetter.py +++ b/telethon/types/_custom/chatgetter.py @@ -1,6 +1,7 @@ import abc -from ... import errors, utils, _tl +from ..._misc import utils +from ... import errors, _tl class ChatGetter(abc.ABC): From 1c15375ea41183c42a7e6122c8744bc5db403822 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 28 Sep 2021 21:06:00 +0200 Subject: [PATCH 104/256] Fix get_participants was monkey-patching User It no longer can do that. User has __slots__. --- telethon/_client/chats.py | 3 --- telethon/_client/telegramclient.py | 5 +---- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/telethon/_client/chats.py b/telethon/_client/chats.py index ffb68c0f..693feeab 100644 --- a/telethon/_client/chats.py +++ b/telethon/_client/chats.py @@ -167,7 +167,6 @@ class _ParticipantsIter(requestiter.RequestIter): continue user = users[user_id] - user.participant = participant self.buffer.append(user) return True @@ -176,7 +175,6 @@ class _ParticipantsIter(requestiter.RequestIter): if self.limit != 0: user = await self.client.get_entity(entity) if self.filter_entity(user): - user.participant = None self.buffer.append(user) return True @@ -213,7 +211,6 @@ class _ParticipantsIter(requestiter.RequestIter): continue self.seen.add(user_id) user = users[user_id] - user.participant = participant self.buffer.append(user) diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 136718ab..bc4371f3 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -731,10 +731,7 @@ class TelegramClient: * ``'contact'`` Yields - The :tl:`User` objects returned by :tl:`GetParticipants` - with an additional ``.participant`` attribute which is the - matched :tl:`ChannelParticipant` type for channels/megagroups - or :tl:`ChatParticipants` for normal chats. + The :tl:`User` objects returned by :tl:`GetParticipants`. Example .. code-block:: python From 5a8c066ff7b6b75b88efa524b52dc709a5683959 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 28 Sep 2021 21:07:15 +0200 Subject: [PATCH 105/256] Fix generated RpcError were no longer formatting the value --- telethon/errors/_rpcbase.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/telethon/errors/_rpcbase.py b/telethon/errors/_rpcbase.py index d074be26..c67c6ebf 100644 --- a/telethon/errors/_rpcbase.py +++ b/telethon/errors/_rpcbase.py @@ -17,6 +17,10 @@ _NESTS_QUERY = ( class RpcError(Exception): def __init__(self, code, message, request=None): + # Special-case '2fa' to exclude the 2 from values + self.values = [int(x) for x in re.findall(r'-?\d+', re.sub(r'^2fa', '', message, flags=re.IGNORECASE))] + self.value = self.values[0] if self.values else None + doc = self.__doc__ if doc is None: doc = ( @@ -25,14 +29,13 @@ class RpcError(Exception): ) elif not doc: doc = '(no description available)' + elif self.value: + doc = re.sub(r'{(\w+)}', str(self.value), doc) super().__init__(f'{message}, code={code}{self._fmt_request(request)}: {doc}') self.code = code self.message = message self.request = request - # Special-case '2fa' to exclude the 2 from values - self.values = [int(x) for x in re.findall(r'-?\d+', re.sub(r'^2fa', '', self.message, flags=re.IGNORECASE))] - self.value = self.values[0] if self.values else None @staticmethod def _fmt_request(request): From 3853f98e5f9afd268b899118cc99899b57bc879e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 9 Oct 2021 12:01:45 +0200 Subject: [PATCH 106/256] Begin work into making Message a viable way to send them --- telethon/types/_custom/inputfile.py | 170 +++++++++++++ telethon/types/_custom/inputmessage.py | 29 +++ telethon/types/_custom/message.py | 314 ++++++++++++++++++++++--- 3 files changed, 486 insertions(+), 27 deletions(-) create mode 100644 telethon/types/_custom/inputfile.py create mode 100644 telethon/types/_custom/inputmessage.py diff --git a/telethon/types/_custom/inputfile.py b/telethon/types/_custom/inputfile.py new file mode 100644 index 00000000..115e18a1 --- /dev/null +++ b/telethon/types/_custom/inputfile.py @@ -0,0 +1,170 @@ +import mimetypes +import os +import pathlib + +from ... import _tl + + +class InputFile: + def __init__( + self, + file = None, + *, + file_name: str = None, + mime_type: str = None, + thumb: str = False, + force_file: bool = False, + file_size: int = None, + duration: int = None, + width: int = None, + height: int = None, + title: str = None, + performer: str = None, + supports_streaming: bool = False, + video_note: bool = False, + voice_note: bool = False, + waveform: bytes = None, + ttl: int = None, + ): + if isinstance(file, pathlib.Path): + if not file_name: + file_name = file.name + file = str(file.absolute()) + elif not file_name: + if isinstance(file, str): + file_name = os.path.basename(file) + else: + file_name = getattr(file, 'name', 'unnamed') + + if not mime_type: + mime_type = mimetypes.guess_type(file_name)[0] or 'application/octet-stream' + + mime_type = mime_type.lower() + + attributes = [_tl.DocumentAttributeFilename(file_name)] + + # TODO hachoir or tinytag or ffmpeg + if mime_type.startswith('image'): + if width is not None and height is not None: + attributes.append(_tl.DocumentAttributeImageSize( + w=width, + h=height, + )) + elif mime_type.startswith('audio'): + attributes.append(_tl.DocumentAttributeAudio( + duration=duration, + voice=voice_note, + title=title, + performer=performer, + waveform=waveform, + )) + elif mime_type.startswith('video'): + attributes.append(_tl.DocumentAttributeVideo( + duration=duration, + w=width, + h=height, + round_message=video_note, + supports_streaming=supports_streaming, + )) + + # mime_type: str = None, + # thumb: str = False, + # force_file: bool = False, + # file_size: int = None, + # ttl: int = None, + + self._file = file + self._attributes = attributes + + + # TODO rest + + is_image = utils.is_image(file) + if as_image is None: + as_image = is_image and not force_document + + # `aiofiles` do not base `io.IOBase` but do have `read`, so we + # just check for the read attribute to see if it's file-like. + if not isinstance(file, (str, bytes, _tl.InputFile, _tl.InputFileBig))\ + and not hasattr(file, 'read'): + # The user may pass a Message containing media (or the media, + # or anything similar) that should be treated as a file. Try + # getting the input media for whatever they passed and send it. + # + # We pass all attributes since these will be used if the user + # passed :tl:`InputFile`, and all information may be relevant. + try: + return (None, utils.get_input_media( + file, + is_photo=as_image, + attributes=attributes, + force_document=force_document, + voice_note=voice_note, + video_note=video_note, + supports_streaming=supports_streaming, + ttl=ttl + ), as_image) + except TypeError: + # Can't turn whatever was given into media + return None, None, as_image + + media = None + file_handle = None + + if isinstance(file, (_tl.InputFile, _tl.InputFileBig)): + file_handle = file + elif not isinstance(file, str) or os.path.isfile(file): + file_handle = await self.upload_file( + _resize_photo_if_needed(file, as_image), + file_size=file_size, + progress_callback=progress_callback + ) + elif re.match('https?://', file): + if as_image: + media = _tl.InputMediaPhotoExternal(file, ttl_seconds=ttl) + else: + media = _tl.InputMediaDocumentExternal(file, ttl_seconds=ttl) + + if media: + pass # Already have media, don't check the rest + elif not file_handle: + raise ValueError( + 'Failed to convert {} to media. Not an existing file or ' + 'HTTP URL'.format(file) + ) + elif as_image: + media = _tl.InputMediaUploadedPhoto(file_handle, ttl_seconds=ttl) + else: + attributes, mime_type = utils.get_attributes( + file, + mime_type=mime_type, + attributes=attributes, + force_document=force_document and not is_image, + voice_note=voice_note, + video_note=video_note, + supports_streaming=supports_streaming, + thumb=thumb + ) + + if not thumb: + thumb = None + else: + if isinstance(thumb, pathlib.Path): + thumb = str(thumb.absolute()) + thumb = await self.upload_file(thumb, file_size=file_size) + + media = _tl.InputMediaUploadedDocument( + file=file_handle, + mime_type=mime_type, + attributes=attributes, + thumb=thumb, + force_file=force_document and not is_image, + ttl_seconds=ttl + ) + return file_handle, media, as_image + + + + + + diff --git a/telethon/types/_custom/inputmessage.py b/telethon/types/_custom/inputmessage.py new file mode 100644 index 00000000..30b0b079 --- /dev/null +++ b/telethon/types/_custom/inputmessage.py @@ -0,0 +1,29 @@ + +class InputMessage: + __slots__ = ( + '_text', + '_link_preview', + '_silent', + '_reply_markup', + '_fmt_entities', + '_file', + ) + + def __init__( + self, + text, + *, + link_preview, + silent, + reply_markup, + fmt_entities, + file, + ): + self._text = text + self._link_preview = link_preview + self._silent = silent + self._reply_markup = reply_markup + self._fmt_entities = fmt_entities + self._file = file + + # oh! when this message is used, the file can be cached in here! if not inputfile upload and set inputfile diff --git a/telethon/types/_custom/message.py b/telethon/types/_custom/message.py index ef7cf734..7e9f0ba0 100644 --- a/telethon/types/_custom/message.py +++ b/telethon/types/_custom/message.py @@ -1,12 +1,20 @@ from typing import Optional, List, TYPE_CHECKING from datetime import datetime +import mimetypes from .chatgetter import ChatGetter from .sendergetter import SenderGetter from .messagebutton import MessageButton from .forward import Forward from .file import File +from .inputfile import InputFile +from .inputmessage import InputMessage +from .button import build_reply_markup from ..._misc import utils, tlobject -from ... import _tl +from ... import _tl, _misc + + +if TYPE_CHECKING: + from ..._misc import hints def _fwd(field, doc): @@ -23,13 +31,19 @@ def _fwd(field, doc): # Maybe parsing the init function alone if that's possible. class Message(ChatGetter, SenderGetter): """ - This custom class aggregates both :tl:`Message` and - :tl:`MessageService` to ease accessing their members. + Represents a :tl:`Message` (or :tl:`MessageService`) from the API. Remember that this class implements `ChatGetter ` and `SenderGetter ` which means you have access to all their sender and chat properties and methods. + + You can also create your own instance of this type to customize how a + message should be sent (rather than just plain text). For example, you + can create an instance with a text to be used for the caption of an audio + file with a certain performer, duration and thumbnail. However, most + properties and methods won't work (since messages you create have not yet + been sent). """ # region Forwarded properties @@ -150,7 +164,10 @@ class Message(ChatGetter, SenderGetter): @media.setter def media(self, value): - self._message.media = value + try: + self._message.media = value + except AttributeError: + pass reply_markup = _fwd('reply_markup', """ The reply markup for this message (which was sent @@ -211,12 +228,230 @@ class Message(ChatGetter, SenderGetter): # region Initialization - def __init__(self, client, message): + _default_parse_mode = None + _default_link_preview = True + + def __init__( + self, + text: str = None, + *, + # Formatting + markdown: str = None, + html: str = None, + formatting_entities: list = None, + link_preview: bool = (), + # Media + file: Optional[hints.FileLike] = None, + file_name: str = None, + mime_type: str = None, + thumb: str = False, + force_file: bool = False, + file_size: int = None, + # Media attributes + duration: int = None, + width: int = None, + height: int = None, + title: str = None, + performer: str = None, + supports_streaming: bool = False, + video_note: bool = False, + voice_note: bool = False, + waveform: bytes = None, + # Additional parametrization + silent: bool = False, + buttons: list = None, + ttl: int = None, + ): + """ + The input parameters when creating a new message for sending are: + + :param text: The message text (also known as caption when including media). + This will be parsed according to the default parse mode, which can be changed with + ``set_default_parse_mode``. + + By default it's markdown if the ``markdown-it-py`` package is installed, or none otherwise. + Cannot be used in conjunction with ``text`` or ``html``. + + :param markdown: Sets the text, but forces the parse mode to be markdown. + Cannot be used in conjunction with ``text`` or ``html``. + + :param html: Sets the text, but forces the parse mode to be HTML. + Cannot be used in conjunction with ``text`` or ``markdown``. + + :param formatting_entities: Manually specifies the formatting entities. + Neither of ``text``, ``markdown`` or ``html`` will be processed. + + :param link_preview: Whether to include a link preview media in the message. + The default is to show it, but this can be changed with ``set_default_link_preview``. + Has no effect if the message contains other media (such as photos). + + :param file: Send a file. The library will automatically determine whether to send the + file as a photo or as a document based on the extension. You can force a specific type + by using ``photo`` or ``document`` instead. The file can be one of: + + * A local file path to an in-disk file. The file name will default to the path's base name. + + * A `bytes` byte array with the file's data to send (for example, by using + ``text.encode('utf-8')``). A default file name will be used. + + * A bytes `io.IOBase` stream over the file to send (for example, by using + ``open(file, 'rb')``). Its ``.name`` property will be used for the file name, or a + default if it doesn't have one. + + * An external URL to a file over the internet. This will send the file as "external" + media, and Telegram is the one that will fetch the media and send it. This means + the library won't download the file to send it first, but Telegram may fail to access + the media. The URL must start with either ``'http://'`` or ``https://``. + + * A handle to an existing file (for example, if you sent a message with media before, + you can use its ``message.media`` as a file here). + + * A :tl:`InputMedia` instance. For example, if you want to send a dice use + :tl:`InputMediaDice`, or if you want to send a contact use :tl:`InputMediaContact`. + + :param file_name: Forces a specific file name to be used, rather than an automatically + determined one. Has no effect with previously-sent media. + + :param mime_type: Sets a fixed mime type for the file, rather than having the library + guess it from the final file name. Useful when an URL does not contain an extension. + The mime-type will be used to determine which media attributes to include (for instance, + whether to send a video, an audio, or a photo). + + * For an image to contain an image size, you must specify width and height. + * For an audio, you must specify the duration. + * For a video, you must specify width, height and duration. + + :param thumb: A file to be used as the document's thumbnail. Only has effect on uploaded + documents. + + :param force_file: Forces whatever file was specified to be sent as a file. + Has no effect with previously-sent media. + + :param file_size: The size of the file to be uploaded if it needs to be uploaded, which + will be determined automatically if not specified. If the file size can't be determined + beforehand, the entire file will be read in-memory to find out how large it is. Telegram + requires the file size to be known before-hand (except for external media). + + :param duration: Specifies the duration, in seconds, of the audio or video file. Only has + effect on uploaded documents. + + :param width: Specifies the photo or video width, in pixels. Only has an effect on uploaded + documents. + + :param height: Specifies the photo or video height, in pixels. Only has an effect on + uploaded documents. + + :param title: Specifies the title of the song being sent. Only has effect on uploaded + documents. You must specify the audio duration. + + :param performer: Specifies the performer of the song being sent. Only has effect on + uploaded documents. You must specify the audio duration. + + :param supports_streaming: Whether the video has been recorded in such a way that it + supports streaming. Note that not all format can support streaming. Only has effect on + uploaded documents. You must specify the video duration, width and height. + + :param video_note: Whether the video should be a "video note" and render inside a circle. + Only has effect on uploaded documents. You must specify the video duration, width and + height. + + :param voice_note: Whether the audio should be a "voice note" and render with a waveform. + Only has effect on uploaded documents. You must specify the audio duration. + + :param waveform: The waveform. You must specify the audio duration. + + :param silent: Whether the message should notify people with sound or not. By default, a + notification with sound is sent unless the person has the chat muted). + + :param buttons: The matrix (list of lists), column list or button to be shown after + sending the message. This parameter will only work if you have signed in as a bot. + + :param schedule: If set, the message won't send immediately, and instead it will be + scheduled to be automatically sent at a later time. + + :param ttl: The Time-To-Live of the file (also known as "self-destruct timer" or + "self-destructing media"). If set, files can only be viewed for a short period of time + before they disappear from the message history automatically. + + The value must be at least 1 second, and at most 60 seconds, otherwise Telegram will + ignore this parameter. + + Not all types of media can be used with this parameter, such as text documents, which + will fail with ``TtlMediaInvalidError``. + """ + if (text and markdown) or (text and html) or (markdown and html): + raise ValueError('can only set one of: text, markdown, html') + + if formatting_entities: + text = text or markdown or html + elif text: + text, formatting_entities = self._default_parse_mode[0](text) + elif markdown: + text, formatting_entities = _misc.markdown.parse(markdown) + elif html: + text, formatting_entities = _misc.html.parse(html) + + reply_markup = build_reply_markup(buttons) if buttons else None + + if not text: + text = '' + if not formatting_entities: + formatting_entities = None + + if link_preview == (): + link_preview = self._default_link_preview + + if file: + file = InputFile( + file=file, + file_name=file_name, + mime_type=mime_type, + thumb=thumb, + force_file=force_file, + file_size=file_size, + duration=duration, + width=width, + height=height, + title=title, + performer=performer, + supports_streaming=supports_streaming, + video_note=video_note, + voice_note=voice_note, + waveform=waveform, + ) + + self._message = InputMessage( + text=text, + link_preview=link_preview, + silent=silent, + reply_markup=reply_markup, + fmt_entities=formatting_entities, + file=file, + ) + + @classmethod + def _new(cls, client, message, entities, input_chat): + self = cls.__new__(cls) + + sender_id = None + if isinstance(message, _tl.Message): + if message.from_id is not None: + sender_id = utils.get_peer_id(message.from_id) + if sender_id is None and message.peer_id and not isinstance(message, _tl.MessageEmpty): + # If the message comes from a Channel, let the sender be it + # ...or... + # incoming messages in private conversations no longer have from_id + # (layer 119+), but the sender can only be the chat we're in. + if message.post or (not message.out and isinstance(message.peer_id, _tl.PeerUser)): + sender_id = utils.get_peer_id(message.peer_id) + + # Note that these calls would reset the client + ChatGetter.__init__(self, self.peer_id, broadcast=self.post) + SenderGetter.__init__(self, sender_id) self._client = client self._message = message # Convenient storage for custom functions - self._client = None self._text = None self._file = None self._reply_message = None @@ -227,29 +462,8 @@ class Message(ChatGetter, SenderGetter): self._via_input_bot = None self._action_entities = None self._linked_chat = None - - sender_id = None - if self.from_id is not None: - sender_id = utils.get_peer_id(self.from_id) - elif self.peer_id: - # If the message comes from a Channel, let the sender be it - # ...or... - # incoming messages in private conversations no longer have from_id - # (layer 119+), but the sender can only be the chat we're in. - if self.post or (not self.out and isinstance(self.peer_id, _tl.PeerUser)): - sender_id = utils.get_peer_id(self.peer_id) - - # Note that these calls would reset the client - ChatGetter.__init__(self, self.peer_id, broadcast=self.post) - SenderGetter.__init__(self, sender_id) - self._forward = None - @classmethod - def _new(cls, client, message, entities, input_chat): - self = cls(client, message) - self._client = client - # Make messages sent to ourselves outgoing unless they're forwarded. # This makes it consistent with official client's appearance. if self.peer_id == _tl.PeerUser(client._session_state.user_id) and not self.fwd_from: @@ -296,6 +510,49 @@ class Message(ChatGetter, SenderGetter): return self + + @classmethod + def set_default_parse_mode(cls, mode): + """ + Change the default parse mode when creating messages. The ``mode`` can be: + + * ``None``, to disable parsing. + * A string equal to ``'md'`` or ``'markdown`` for parsing with commonmark, + ``'htm'`` or ``'html'`` for parsing HTML. + * A ``callable``, which accepts a ``str`` as input and returns a tuple of + ``(parsed str, formatting entities)``. + * A ``tuple`` of two ``callable``. The first must accept a ``str`` as input and return + a tuple of ``(parsed str, list of formatting entities)``. The second must accept two + parameters, a parsed ``str`` and a ``list`` of formatting entities, and must return + an "unparsed" ``str``. + + If it's not one of these values or types, the method fails accordingly. + """ + if isinstance(mode, str): + mode = mode.lower() + if mode in ('md', 'markdown'): + cls._default_parse_mode = (_misc.markdown.parse, _misc.markdown.unparse) + elif mode in ('htm', 'html'): + cls._default_parse_mode = (_misc.html.parse, _misc.html.unparse) + else: + raise ValueError(f'mode must be one of md, markdown, htm or html, but was {mode!r}') + elif callable(mode): + cls._default_parse_mode = (mode, lambda t, e: t) + elif isinstance(mode, tuple): + if len(mode) == 2 and callable(mode[0]) and callable(mode[1]): + cls._default_parse_mode = mode + else: + raise ValueError(f'mode must be a tuple of exactly two callables') + else: + raise TypeError(f'mode must be either a str, callable or tuple, but was {mode!r}') + + @classmethod + def set_default_link_preview(cls, enabled): + """ + Change the default value for link preview (either ``True`` or ``False``). + """ + cls._default_link_preview = enabled + # endregion Initialization # region Public Properties @@ -1121,3 +1378,6 @@ class Message(ChatGetter, SenderGetter): return None # endregion Private Methods + + +# TODO set md by default if commonmark is installed else nothing From 72fc8f680817ea067b85c06c2d9223b219fd8f06 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 12 Oct 2021 17:59:30 +0200 Subject: [PATCH 107/256] Continue work on Message sending overhaul --- readthedocs/misc/v2-migration-guide.rst | 15 ++ telethon/_client/messages.py | 139 +++++++------ telethon/_client/telegramclient.py | 64 +++--- telethon/_client/uploads.py | 246 ++++++---------------- telethon/types/_custom/inputfile.py | 265 ++++++++++++------------ telethon/types/_custom/inputmessage.py | 81 +++++++- telethon/types/_custom/message.py | 95 ++++----- 7 files changed, 435 insertions(+), 470 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 9444b87c..c16ed02f 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -650,3 +650,18 @@ CdnDecrypter has been removed ----------------------------- It was not really working and was more intended to be an implementation detail than anything else. + + +--- + +you can no longer pass an attributes list because the constructor is now nice. +use raw api if you really need it. +goal is to hide raw api from high level api. sorry. + +no parsemode. use the correct parameter. it's more convenient than setting two. + +formatting_entities stays because otherwise it's the only feasible way to manually specify it. + +todo update send_message and send_file docs (well review all functions) + +album overhaul. use a list of Message instead. diff --git a/telethon/_client/messages.py b/telethon/_client/messages.py index fd3fbea8..4d2ad927 100644 --- a/telethon/_client/messages.py +++ b/telethon/_client/messages.py @@ -1,10 +1,12 @@ import inspect import itertools +import time import typing import warnings from .._misc import helpers, utils, requestiter, hints from ..types import _custom +from ..types._custom.inputmessage import InputMessage from .. import errors, _tl _MAX_CHUNK_SIZE = 100 @@ -395,82 +397,95 @@ async def send_message( entity: 'hints.EntityLike', message: 'hints.MessageLike' = '', *, - reply_to: 'typing.Union[int, _tl.Message]' = None, - attributes: 'typing.Sequence[_tl.TypeDocumentAttribute]' = None, - parse_mode: typing.Optional[str] = (), - formatting_entities: typing.Optional[typing.List[_tl.TypeMessageEntity]] = None, - link_preview: bool = True, - file: 'typing.Union[hints.FileLike, typing.Sequence[hints.FileLike]]' = None, - thumb: 'hints.FileLike' = None, - force_document: bool = False, - clear_draft: bool = False, - buttons: 'hints.MarkupLike' = None, - silent: bool = None, - background: bool = None, + # - Message contents + # Formatting + markdown: str = None, + html: str = None, + formatting_entities: list = None, + link_preview: bool = (), + # Media + file: typing.Optional[hints.FileLike] = None, + file_name: str = None, + mime_type: str = None, + thumb: str = False, + force_file: bool = False, + file_size: int = None, + # Media attributes + duration: int = None, + width: int = None, + height: int = None, + title: str = None, + performer: str = None, supports_streaming: bool = False, + video_note: bool = False, + voice_note: bool = False, + waveform: bytes = None, + # Additional parametrization + silent: bool = False, + buttons: list = None, + ttl: int = None, + # - Send options + reply_to: 'typing.Union[int, _tl.Message]' = None, + clear_draft: bool = False, + background: bool = None, schedule: 'hints.DateLike' = None, - comment_to: 'typing.Union[int, _tl.Message]' = None + comment_to: 'typing.Union[int, _tl.Message]' = None, ) -> '_tl.Message': - if file is not None: - return await self.send_file( - entity, file, caption=message, reply_to=reply_to, - attributes=attributes, parse_mode=parse_mode, - force_document=force_document, thumb=thumb, - buttons=buttons, clear_draft=clear_draft, silent=silent, - schedule=schedule, supports_streaming=supports_streaming, + if isinstance(message, str): + message = InputMessage( + text=message, + markdown=markdown, + html=html, formatting_entities=formatting_entities, - comment_to=comment_to, background=background + link_preview=link_preview, + file=file, + file_name=file_name, + mime_type=mime_type, + thumb=thumb, + force_file=force_file, + file_size=file_size, + duration=duration, + width=width, + height=height, + title=title, + performer=performer, + supports_streaming=supports_streaming, + video_note=video_note, + voice_note=voice_note, + waveform=waveform, + silent=silent, + buttons=buttons, + ttl=ttl, ) + elif isinstance(message, _custom.Message): + message = message._as_input() + elif not isinstance(message, InputMessage): + raise TypeError(f'message must be either str, Message or InputMessage, but got: {message!r}') entity = await self.get_input_entity(entity) if comment_to is not None: entity, reply_to = await _get_comment_data(self, entity, comment_to) + elif reply_to: + reply_to = utils.get_message_id(reply_to) - if isinstance(message, _tl.Message): - if buttons is None: - markup = message.reply_markup - else: - markup = _custom.button.build_reply_markup(buttons) + if message._file: + # TODO Properly implement allow_cache to reuse the sha256 of the file + # i.e. `None` was used - if silent is None: - silent = message.silent + # TODO album + if message._file._should_upload_thumb(): + message._file._set_uploaded_thumb(await self.upload_file(message._file._thumb)) - if (message.media and not isinstance( - message.media, _tl.MessageMediaWebPage)): - return await self.send_file( - entity, - message.media, - caption=message.message, - silent=silent, - background=background, - reply_to=reply_to, - buttons=markup, - formatting_entities=message.entities, - schedule=schedule - ) + if message._file._should_upload_file(): + message._file._set_uploaded_file(await self.upload_file(message._file._file)) - request = _tl.fn.messages.SendMessage( - peer=entity, - message=message.message or '', - silent=silent, - background=background, - reply_to_msg_id=utils.get_message_id(reply_to), - reply_markup=markup, - entities=message.entities, - clear_draft=clear_draft, - no_webpage=not isinstance( - message.media, _tl.MessageMediaWebPage), - schedule_date=schedule + request = _tl.fn.messages.SendMedia( + entity, message._file._media, reply_to_msg_id=reply_to, message=message._text, + entities=message._fmt_entities, reply_markup=message._reply_markup, silent=message._silent, + schedule_date=schedule, clear_draft=clear_draft, + background=background ) - message = message.message else: - if formatting_entities is None: - message, formatting_entities = await self._parse_message_text(message, parse_mode) - if not message: - raise ValueError( - 'The message cannot be empty unless a file is provided' - ) - request = _tl.fn.messages.SendMessage( peer=entity, message=message, @@ -489,7 +504,7 @@ async def send_message( return _custom.Message._new(self, _tl.Message( id=result.id, peer_id=await _get_peer(self, entity), - message=message, + message=message._text, date=result.date, out=result.out, media=result.media, diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index bc4371f3..f2dc4ede 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -2075,31 +2075,46 @@ class TelegramClient: entity: 'hints.EntityLike', message: 'hints.MessageLike' = '', *, - reply_to: 'typing.Union[int, _tl.Message]' = None, - attributes: 'typing.Sequence[_tl.TypeDocumentAttribute]' = None, - parse_mode: typing.Optional[str] = (), - formatting_entities: typing.Optional[typing.List[_tl.TypeMessageEntity]] = None, - link_preview: bool = True, - file: 'typing.Union[hints.FileLike, typing.Sequence[hints.FileLike]]' = None, - thumb: 'hints.FileLike' = None, - force_document: bool = False, - clear_draft: bool = False, - buttons: 'hints.MarkupLike' = None, - silent: bool = None, - background: bool = None, + # - Message contents + # Formatting + markdown: str = None, + html: str = None, + formatting_entities: list = None, + link_preview: bool = (), + # Media + file: 'typing.Optional[hints.FileLike]' = None, + file_name: str = None, + mime_type: str = None, + thumb: str = False, + force_file: bool = False, + file_size: int = None, + # Media attributes + duration: int = None, + width: int = None, + height: int = None, + title: str = None, + performer: str = None, supports_streaming: bool = False, + video_note: bool = False, + voice_note: bool = False, + waveform: bytes = None, + # Additional parametrization + silent: bool = False, + buttons: list = None, + ttl: int = None, + # - Send options + reply_to: 'typing.Union[int, _tl.Message]' = None, + clear_draft: bool = False, + background: bool = None, schedule: 'hints.DateLike' = None, - comment_to: 'typing.Union[int, _tl.Message]' = None + comment_to: 'typing.Union[int, _tl.Message]' = None, ) -> '_tl.Message': """ - Sends a message to the specified user, chat or channel. + Sends a Message to the specified user, chat or channel. - The default parse mode is the same as the official applications - (a _custom flavour of markdown). ``**bold**, `code` or __italic__`` - are available. In addition you can send ``[links](https://example.com)`` - and ``[mentions](@username)`` (or using IDs like in the Bot API: - ``[mention](tg://user?id=123456789)``) and ``pre`` blocks with three - backticks. + The message can be either a string or a previous Message instance. + If it's a previous Message instance, the rest of parameters will be ignored. + If it's not, a Message instance will be constructed, and send_to used. Sending a ``/start`` command with a parameter (like ``?start=data``) is also done through this method. Simply send ``'/start data'`` to @@ -3517,15 +3532,6 @@ class TelegramClient: async def _parse_message_text(self: 'TelegramClient', message, parse_mode): pass - @forward_call(uploads._file_to_media) - async def _file_to_media( - self, file, force_document=False, file_size=None, - progress_callback=None, attributes=None, thumb=None, - allow_cache=True, voice_note=False, video_note=False, - supports_streaming=False, mime_type=None, as_image=None, - ttl=None): - pass - @forward_call(messageparse._get_response_message) def _get_response_message(self: 'TelegramClient', request, result, input_chat): pass diff --git a/telethon/_client/uploads.py b/telethon/_client/uploads.py index 9c203008..5cfd43e4 100644 --- a/telethon/_client/uploads.py +++ b/telethon/_client/uploads.py @@ -92,105 +92,74 @@ def _resize_photo_if_needed( async def send_file( self: 'TelegramClient', entity: 'hints.EntityLike', - file: 'typing.Union[hints.FileLike, typing.Sequence[hints.FileLike]]', + file: typing.Optional[hints.FileLike] = None, *, - caption: typing.Union[str, typing.Sequence[str]] = None, - force_document: bool = False, + # - Message contents + # Formatting + caption: 'hints.MessageLike' = '', + markdown: str = None, + html: str = None, + formatting_entities: list = None, + link_preview: bool = (), + # Media + file_name: str = None, + mime_type: str = None, + thumb: str = False, + force_file: bool = False, file_size: int = None, - clear_draft: bool = False, - progress_callback: 'hints.ProgressCallback' = None, - reply_to: 'hints.MessageIDLike' = None, - attributes: 'typing.Sequence[_tl.TypeDocumentAttribute]' = None, - thumb: 'hints.FileLike' = None, - allow_cache: bool = True, - parse_mode: str = (), - formatting_entities: typing.Optional[typing.List[_tl.TypeMessageEntity]] = None, - voice_note: bool = False, - video_note: bool = False, - buttons: 'hints.MarkupLike' = None, - silent: bool = None, - background: bool = None, + # Media attributes + duration: int = None, + width: int = None, + height: int = None, + title: str = None, + performer: str = None, supports_streaming: bool = False, + video_note: bool = False, + voice_note: bool = False, + waveform: bytes = None, + # Additional parametrization + silent: bool = False, + buttons: list = None, + ttl: int = None, + # - Send options + reply_to: 'typing.Union[int, _tl.Message]' = None, + clear_draft: bool = False, + background: bool = None, schedule: 'hints.DateLike' = None, comment_to: 'typing.Union[int, _tl.Message]' = None, - ttl: int = None, - **kwargs) -> '_tl.Message': - # TODO Properly implement allow_cache to reuse the sha256 of the file - # i.e. `None` was used - if not file: - raise TypeError('Cannot use {!r} as file'.format(file)) - - if not caption: - caption = '' - - entity = await self.get_input_entity(entity) - if comment_to is not None: - entity, reply_to = await _get_comment_data(self, entity, comment_to) - else: - reply_to = utils.get_message_id(reply_to) - - # First check if the user passed an iterable, in which case - # we may want to send grouped. - if utils.is_list_like(file): - if utils.is_list_like(caption): - captions = caption - else: - captions = [caption] - - result = [] - while file: - result += await _send_album( - self, entity, file[:10], caption=captions[:10], - progress_callback=progress_callback, reply_to=reply_to, - parse_mode=parse_mode, silent=silent, schedule=schedule, - supports_streaming=supports_streaming, clear_draft=clear_draft, - force_document=force_document, background=background, - ) - file = file[10:] - captions = captions[10:] - - for doc, cap in zip(file, captions): - result.append(await self.send_file( - entity, doc, allow_cache=allow_cache, - caption=cap, force_document=force_document, - progress_callback=progress_callback, reply_to=reply_to, - attributes=attributes, thumb=thumb, voice_note=voice_note, - video_note=video_note, buttons=buttons, silent=silent, - supports_streaming=supports_streaming, schedule=schedule, - clear_draft=clear_draft, background=background, - **kwargs - )) - - return result - - if formatting_entities is not None: - msg_entities = formatting_entities - else: - caption, msg_entities =\ - await self._parse_message_text(caption, parse_mode) - - file_handle, media, image = await _file_to_media( - self, file, force_document=force_document, +) -> '_tl.Message': + self.send_message( + entity=entity, + message=caption, + markdown=markdown, + html=html, + formatting_entities=formatting_entities, + link_preview=link_preview, + file=file, + file_name=file_name, + mime_type=mime_type, + thumb=thumb, + force_file=force_file, file_size=file_size, - progress_callback=progress_callback, - attributes=attributes, allow_cache=allow_cache, thumb=thumb, - voice_note=voice_note, video_note=video_note, - supports_streaming=supports_streaming, ttl=ttl + duration=duration, + width=width, + height=height, + title=title, + performer=performer, + supports_streaming=supports_streaming, + video_note=video_note, + voice_note=voice_note, + waveform=waveform, + silent=silent, + buttons=buttons, + ttl=ttl, + reply_to=reply_to, + clear_draft=clear_draft, + background=background, + schedule=schedule, + comment_to=comment_to, ) - # e.g. invalid cast from :tl:`MessageMediaWebPage` - if not media: - raise TypeError('Cannot use {!r} as file'.format(file)) - - markup = _custom.button.build_reply_markup(buttons) - request = _tl.fn.messages.SendMedia( - entity, media, reply_to_msg_id=reply_to, message=caption, - entities=msg_entities, reply_markup=markup, silent=silent, - schedule_date=schedule, clear_draft=clear_draft, - background=background - ) - return self._get_response_message(request, await self(request), entity) - async def _send_album(self: 'TelegramClient', entity, files, caption='', progress_callback=None, reply_to=None, parse_mode=(), silent=None, schedule=None, @@ -368,98 +337,3 @@ async def upload_file( ) -async def _file_to_media( - self, file, force_document=False, file_size=None, - progress_callback=None, attributes=None, thumb=None, - allow_cache=True, voice_note=False, video_note=False, - supports_streaming=False, mime_type=None, as_image=None, - ttl=None): - if not file: - return None, None, None - - if isinstance(file, pathlib.Path): - file = str(file.absolute()) - - is_image = utils.is_image(file) - if as_image is None: - as_image = is_image and not force_document - - # `aiofiles` do not base `io.IOBase` but do have `read`, so we - # just check for the read attribute to see if it's file-like. - if not isinstance(file, (str, bytes, _tl.InputFile, _tl.InputFileBig))\ - and not hasattr(file, 'read'): - # The user may pass a Message containing media (or the media, - # or anything similar) that should be treated as a file. Try - # getting the input media for whatever they passed and send it. - # - # We pass all attributes since these will be used if the user - # passed :tl:`InputFile`, and all information may be relevant. - try: - return (None, utils.get_input_media( - file, - is_photo=as_image, - attributes=attributes, - force_document=force_document, - voice_note=voice_note, - video_note=video_note, - supports_streaming=supports_streaming, - ttl=ttl - ), as_image) - except TypeError: - # Can't turn whatever was given into media - return None, None, as_image - - media = None - file_handle = None - - if isinstance(file, (_tl.InputFile, _tl.InputFileBig)): - file_handle = file - elif not isinstance(file, str) or os.path.isfile(file): - file_handle = await self.upload_file( - _resize_photo_if_needed(file, as_image), - file_size=file_size, - progress_callback=progress_callback - ) - elif re.match('https?://', file): - if as_image: - media = _tl.InputMediaPhotoExternal(file, ttl_seconds=ttl) - else: - media = _tl.InputMediaDocumentExternal(file, ttl_seconds=ttl) - - if media: - pass # Already have media, don't check the rest - elif not file_handle: - raise ValueError( - 'Failed to convert {} to media. Not an existing file or ' - 'HTTP URL'.format(file) - ) - elif as_image: - media = _tl.InputMediaUploadedPhoto(file_handle, ttl_seconds=ttl) - else: - attributes, mime_type = utils.get_attributes( - file, - mime_type=mime_type, - attributes=attributes, - force_document=force_document and not is_image, - voice_note=voice_note, - video_note=video_note, - supports_streaming=supports_streaming, - thumb=thumb - ) - - if not thumb: - thumb = None - else: - if isinstance(thumb, pathlib.Path): - thumb = str(thumb.absolute()) - thumb = await self.upload_file(thumb, file_size=file_size) - - media = _tl.InputMediaUploadedDocument( - file=file_handle, - mime_type=mime_type, - attributes=attributes, - thumb=thumb, - force_file=force_document and not is_image, - ttl_seconds=ttl - ) - return file_handle, media, as_image diff --git a/telethon/types/_custom/inputfile.py b/telethon/types/_custom/inputfile.py index 115e18a1..d505fd11 100644 --- a/telethon/types/_custom/inputfile.py +++ b/telethon/types/_custom/inputfile.py @@ -1,11 +1,36 @@ import mimetypes import os -import pathlib +import re +import time + +from pathlib import Path from ... import _tl +from ..._misc import utils class InputFile: + # Expected Time-To-Live for _uploaded_*. + # After this period they should be reuploaded. + # Telegram's limit are unknown, so this value is conservative. + UPLOAD_TTL = 8 * 60 * 60 + + __slots__ = ( + # main media + '_file', # can reupload + '_media', # can only use as-is + '_uploaded_file', # (input file, timestamp) + # thumbnail + '_thumb', # can reupload + '_uploaded_thumb', # (input file, timestamp) + # document parameters + '_mime_type', + '_attributes', + '_video_note', + '_force_file', + '_ttl', + ) + def __init__( self, file = None, @@ -26,145 +51,125 @@ class InputFile: waveform: bytes = None, ttl: int = None, ): - if isinstance(file, pathlib.Path): - if not file_name: - file_name = file.name - file = str(file.absolute()) - elif not file_name: - if isinstance(file, str): - file_name = os.path.basename(file) + # main media + self._file = None + self._media = None + self._uploaded_file = None + + if isinstance(file, str) and re.match('https?://', file, flags=re.IGNORECASE): + if not force_file and mime_type.startswith('image'): + self._media = _tl.InputMediaPhotoExternal(file, ttl_seconds=ttl) else: - file_name = getattr(file, 'name', 'unnamed') + self._media = _tl.InputMediaDocumentExternal(file, ttl_seconds=ttl) - if not mime_type: - mime_type = mimetypes.guess_type(file_name)[0] or 'application/octet-stream' + elif isinstance(file, (str, bytes, Path)) or callable(getattr(file, 'read', None)): + self._file = file - mime_type = mime_type.lower() + elif isinstance(file, (_tl.InputFile, _tl.InputFileBig)): + self._uploaded_file = (file, time.time()) - attributes = [_tl.DocumentAttributeFilename(file_name)] - - # TODO hachoir or tinytag or ffmpeg - if mime_type.startswith('image'): - if width is not None and height is not None: - attributes.append(_tl.DocumentAttributeImageSize( - w=width, - h=height, - )) - elif mime_type.startswith('audio'): - attributes.append(_tl.DocumentAttributeAudio( - duration=duration, - voice=voice_note, - title=title, - performer=performer, - waveform=waveform, - )) - elif mime_type.startswith('video'): - attributes.append(_tl.DocumentAttributeVideo( - duration=duration, - w=width, - h=height, - round_message=video_note, - supports_streaming=supports_streaming, - )) - - # mime_type: str = None, - # thumb: str = False, - # force_file: bool = False, - # file_size: int = None, - # ttl: int = None, - - self._file = file - self._attributes = attributes - - - # TODO rest - - is_image = utils.is_image(file) - if as_image is None: - as_image = is_image and not force_document - - # `aiofiles` do not base `io.IOBase` but do have `read`, so we - # just check for the read attribute to see if it's file-like. - if not isinstance(file, (str, bytes, _tl.InputFile, _tl.InputFileBig))\ - and not hasattr(file, 'read'): - # The user may pass a Message containing media (or the media, - # or anything similar) that should be treated as a file. Try - # getting the input media for whatever they passed and send it. - # - # We pass all attributes since these will be used if the user - # passed :tl:`InputFile`, and all information may be relevant. - try: - return (None, utils.get_input_media( - file, - is_photo=as_image, - attributes=attributes, - force_document=force_document, - voice_note=voice_note, - video_note=video_note, - supports_streaming=supports_streaming, - ttl=ttl - ), as_image) - except TypeError: - # Can't turn whatever was given into media - return None, None, as_image - - media = None - file_handle = None - - if isinstance(file, (_tl.InputFile, _tl.InputFileBig)): - file_handle = file - elif not isinstance(file, str) or os.path.isfile(file): - file_handle = await self.upload_file( - _resize_photo_if_needed(file, as_image), - file_size=file_size, - progress_callback=progress_callback - ) - elif re.match('https?://', file): - if as_image: - media = _tl.InputMediaPhotoExternal(file, ttl_seconds=ttl) - else: - media = _tl.InputMediaDocumentExternal(file, ttl_seconds=ttl) - - if media: - pass # Already have media, don't check the rest - elif not file_handle: - raise ValueError( - 'Failed to convert {} to media. Not an existing file or ' - 'HTTP URL'.format(file) - ) - elif as_image: - media = _tl.InputMediaUploadedPhoto(file_handle, ttl_seconds=ttl) else: - attributes, mime_type = utils.get_attributes( + self._media = utils.get_input_media( file, - mime_type=mime_type, - attributes=attributes, - force_document=force_document and not is_image, + is_photo=not force_file and mime_type.startswith('image'), + attributes=[], + force_document=force_file, voice_note=voice_note, video_note=video_note, supports_streaming=supports_streaming, - thumb=thumb + ttl=ttl ) - if not thumb: - thumb = None - else: - if isinstance(thumb, pathlib.Path): - thumb = str(thumb.absolute()) - thumb = await self.upload_file(thumb, file_size=file_size) + # thumbnail + self._thumb = None + self._uploaded_thumb = None - media = _tl.InputMediaUploadedDocument( - file=file_handle, - mime_type=mime_type, - attributes=attributes, - thumb=thumb, - force_file=force_document and not is_image, - ttl_seconds=ttl + if isinstance(thumb, (str, bytes, Path)) or callable(getattr(thumb, 'read', None)): + self._thumb = thumb + + elif isinstance(thumb, (_tl.InputFile, _tl.InputFileBig)): + self._uploaded_thumb = (thumb, time.time()) + + else: + raise TypeError(f'thumb must be a file to upload, but got: {thumb!r}') + + # document parameters (only if it's our file, i.e. there's no media ready yet) + if self._media: + self._mime_type = None + self._attributes = None + self._video_note = None + self._force_file = None + self._ttl = None + else: + if isinstance(file, Path): + if not file_name: + file_name = file.name + file = str(file.absolute()) + elif not file_name: + if isinstance(file, str): + file_name = os.path.basename(file) + else: + file_name = getattr(file, 'name', 'unnamed') + + if not mime_type: + mime_type = mimetypes.guess_type(file_name)[0] or 'application/octet-stream' + + mime_type = mime_type.lower() + + attributes = [_tl.DocumentAttributeFilename(file_name)] + + # TODO hachoir or tinytag or ffmpeg + if mime_type.startswith('image'): + if width is not None and height is not None: + attributes.append(_tl.DocumentAttributeImageSize( + w=width, + h=height, + )) + elif mime_type.startswith('audio'): + attributes.append(_tl.DocumentAttributeAudio( + duration=duration, + voice=voice_note, + title=title, + performer=performer, + waveform=waveform, + )) + elif mime_type.startswith('video'): + attributes.append(_tl.DocumentAttributeVideo( + duration=duration, + w=width, + h=height, + round_message=video_note, + supports_streaming=supports_streaming, + )) + + self._mime_type = mime_type + self._attributes = attributes + self._video_note = video_note + self._force_file = force_file + self._ttl = ttl + + def _should_upload_thumb(self): + return self._thumb and ( + not self._uploaded_thumb + or time.time() > self._uploaded_thumb[1] + InputFile.UPLOAD_TTL) + + def _should_upload_file(self): + return self._file and ( + not self._uploaded_file + or time.time() > self._uploaded_file[1] + InputFile.UPLOAD_TTL) + + def _set_uploaded_thumb(self, input_file): + self._uploaded_thumb = (input_file, time.time()) + + def _set_uploaded_file(self, input_file): + if not self._force_file and self._mime_type.startswith('image'): + self._media = _tl.InputMediaUploadedPhoto(input_file, ttl_seconds=self._ttl) + else: + self._media = _tl.InputMediaUploadedDocument( + file=input_file, + mime_type=self._mime_type, + attributes=self._attributes, + thumb=self._uploaded_thumb[0] if self._uploaded_thumb else None, + force_file=self._force_file, + ttl_seconds=self._ttl, ) - return file_handle, media, as_image - - - - - - diff --git a/telethon/types/_custom/inputmessage.py b/telethon/types/_custom/inputmessage.py index 30b0b079..90e1f124 100644 --- a/telethon/types/_custom/inputmessage.py +++ b/telethon/types/_custom/inputmessage.py @@ -1,3 +1,8 @@ +from typing import Optional +from .inputfile import InputFile +from ... import _misc +from .button import build_reply_markup + class InputMessage: __slots__ = ( @@ -9,21 +14,83 @@ class InputMessage: '_file', ) + _default_parse_mode = (lambda t: (t, []), lambda t, e: t) + _default_link_preview = True + def __init__( self, - text, + text: str = None, *, - link_preview, - silent, - reply_markup, - fmt_entities, - file, + markdown: str = None, + html: str = None, + formatting_entities: list = None, + link_preview: bool = (), + file=None, + file_name: str = None, + mime_type: str = None, + thumb: str = False, + force_file: bool = False, + file_size: int = None, + duration: int = None, + width: int = None, + height: int = None, + title: str = None, + performer: str = None, + supports_streaming: bool = False, + video_note: bool = False, + voice_note: bool = False, + waveform: bytes = None, + silent: bool = False, + buttons: list = None, + ttl: int = None, + parse_fn = None, ): + if (text and markdown) or (text and html) or (markdown and html): + raise ValueError('can only set one of: text, markdown, html') + + if formatting_entities: + text = text or markdown or html + elif text: + text, formatting_entities = self._default_parse_mode[0](text) + elif markdown: + text, formatting_entities = _misc.markdown.parse(markdown) + elif html: + text, formatting_entities = _misc.html.parse(html) + + reply_markup = build_reply_markup(buttons) if buttons else None + + if not text: + text = '' + if not formatting_entities: + formatting_entities = None + + if link_preview == (): + link_preview = self._default_link_preview + + if file and not isinstance(file, InputFile): + file = InputFile( + file=file, + file_name=file_name, + mime_type=mime_type, + thumb=thumb, + force_file=force_file, + file_size=file_size, + duration=duration, + width=width, + height=height, + title=title, + performer=performer, + supports_streaming=supports_streaming, + video_note=video_note, + voice_note=voice_note, + waveform=waveform, + ) + self._text = text self._link_preview = link_preview self._silent = silent self._reply_markup = reply_markup - self._fmt_entities = fmt_entities + self._fmt_entities = formatting_entities self._file = file # oh! when this message is used, the file can be cached in here! if not inputfile upload and set inputfile diff --git a/telethon/types/_custom/message.py b/telethon/types/_custom/message.py index 7e9f0ba0..7222fb00 100644 --- a/telethon/types/_custom/message.py +++ b/telethon/types/_custom/message.py @@ -228,9 +228,6 @@ class Message(ChatGetter, SenderGetter): # region Initialization - _default_parse_mode = None - _default_link_preview = True - def __init__( self, text: str = None, @@ -241,7 +238,7 @@ class Message(ChatGetter, SenderGetter): formatting_entities: list = None, link_preview: bool = (), # Media - file: Optional[hints.FileLike] = None, + file: 'Optional[hints.FileLike]' = None, file_name: str = None, mime_type: str = None, thumb: str = False, @@ -379,54 +376,30 @@ class Message(ChatGetter, SenderGetter): Not all types of media can be used with this parameter, such as text documents, which will fail with ``TtlMediaInvalidError``. """ - if (text and markdown) or (text and html) or (markdown and html): - raise ValueError('can only set one of: text, markdown, html') - - if formatting_entities: - text = text or markdown or html - elif text: - text, formatting_entities = self._default_parse_mode[0](text) - elif markdown: - text, formatting_entities = _misc.markdown.parse(markdown) - elif html: - text, formatting_entities = _misc.html.parse(html) - - reply_markup = build_reply_markup(buttons) if buttons else None - - if not text: - text = '' - if not formatting_entities: - formatting_entities = None - - if link_preview == (): - link_preview = self._default_link_preview - - if file: - file = InputFile( - file=file, - file_name=file_name, - mime_type=mime_type, - thumb=thumb, - force_file=force_file, - file_size=file_size, - duration=duration, - width=width, - height=height, - title=title, - performer=performer, - supports_streaming=supports_streaming, - video_note=video_note, - voice_note=voice_note, - waveform=waveform, - ) - self._message = InputMessage( text=text, + markdown=markdown, + html=html, + formatting_entities=formatting_entities, link_preview=link_preview, + file =file, + file_name=file_name, + mime_type=mime_type, + thumb=thumb, + force_file=force_file, + file_size=file_size, + duration=duration, + width=width, + height=height, + title=title, + performer=performer, + supports_streaming=supports_streaming, + video_note=video_note, + voice_note=voice_note, + waveform=waveform, silent=silent, - reply_markup=reply_markup, - fmt_entities=formatting_entities, - file=file, + buttons=buttons, + ttl=ttl, ) @classmethod @@ -446,7 +419,7 @@ class Message(ChatGetter, SenderGetter): sender_id = utils.get_peer_id(message.peer_id) # Note that these calls would reset the client - ChatGetter.__init__(self, self.peer_id, broadcast=self.post) + ChatGetter.__init__(self, message.peer_id, broadcast=message.post) SenderGetter.__init__(self, sender_id) self._client = client self._message = message @@ -511,8 +484,8 @@ class Message(ChatGetter, SenderGetter): return self - @classmethod - def set_default_parse_mode(cls, mode): + @staticmethod + def set_default_parse_mode(mode): """ Change the default parse mode when creating messages. The ``mode`` can be: @@ -531,27 +504,29 @@ class Message(ChatGetter, SenderGetter): if isinstance(mode, str): mode = mode.lower() if mode in ('md', 'markdown'): - cls._default_parse_mode = (_misc.markdown.parse, _misc.markdown.unparse) + mode = (_misc.markdown.parse, _misc.markdown.unparse) elif mode in ('htm', 'html'): - cls._default_parse_mode = (_misc.html.parse, _misc.html.unparse) + mode = (_misc.html.parse, _misc.html.unparse) else: raise ValueError(f'mode must be one of md, markdown, htm or html, but was {mode!r}') elif callable(mode): - cls._default_parse_mode = (mode, lambda t, e: t) + mode = (mode, lambda t, e: t) elif isinstance(mode, tuple): if len(mode) == 2 and callable(mode[0]) and callable(mode[1]): - cls._default_parse_mode = mode + mode = mode else: raise ValueError(f'mode must be a tuple of exactly two callables') else: raise TypeError(f'mode must be either a str, callable or tuple, but was {mode!r}') + InputMessage._default_parse_mode = mode + @classmethod def set_default_link_preview(cls, enabled): """ Change the default value for link preview (either ``True`` or ``False``). """ - cls._default_link_preview = enabled + InputMessage._default_link_preview = enabled # endregion Initialization @@ -1297,6 +1272,14 @@ class Message(ChatGetter, SenderGetter): # region Private Methods + def _as_input(self): + if isinstance(self._message, InputMessage): + return self._message + + return InputMessage( + + ) + async def _reload_message(self): """ Re-fetches this message to reload the sender and chat entities, From a5dce81d0fdc6f527021d958cb3e47436abeb66b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 12 Oct 2021 18:01:34 +0200 Subject: [PATCH 108/256] Actually fill parameters in Message._as_input --- telethon/types/_custom/message.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/telethon/types/_custom/message.py b/telethon/types/_custom/message.py index 7222fb00..103da65b 100644 --- a/telethon/types/_custom/message.py +++ b/telethon/types/_custom/message.py @@ -1277,7 +1277,11 @@ class Message(ChatGetter, SenderGetter): return self._message return InputMessage( - + text=self.message, + formatting_entities=self.entities, + file=self.media, + silent=self.silent, + buttons=self.reply_markup, ) async def _reload_message(self): From 9c796e8d73513810e9008cb1168add9105f2ca9f Mon Sep 17 00:00:00 2001 From: Shrimadhav U K Date: Wed, 1 Dec 2021 23:30:29 +0530 Subject: [PATCH 109/256] Fix typo, add errors, and update to 133 again (#3157) --- telethon_generator/data/api.tl | 4 ++-- telethon_generator/data/errors.csv | 4 ++++ telethon_generator/data/methods.csv | 14 +++++++------- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/telethon_generator/data/api.tl b/telethon_generator/data/api.tl index d9b3d1d7..d1f44341 100644 --- a/telethon_generator/data/api.tl +++ b/telethon_generator/data/api.tl @@ -467,7 +467,7 @@ sendMessageUploadRoundAction#243e1c66 progress:int = SendMessageAction; speakingInGroupCallAction#d92c2285 = SendMessageAction; sendMessageHistoryImportAction#dbda9246 progress:int = SendMessageAction; sendMessageChooseStickerAction#b05ac6b1 = SendMessageAction; -sendMessageEmojiInteraction#6a3233b6 emoticon:string interaction:DataJSON = SendMessageAction; +sendMessageEmojiInteraction#25972bcb emoticon:string msg_id:int interaction:DataJSON = SendMessageAction; sendMessageEmojiInteractionSeen#b665902e emoticon:string = SendMessageAction; contacts.found#b3134d9d my_results:Vector results:Vector chats:Vector users:Vector = contacts.Found; @@ -560,6 +560,7 @@ inputStickerSetID#9de7a269 id:long access_hash:long = InputStickerSet; inputStickerSetShortName#861cc8a0 short_name:string = InputStickerSet; inputStickerSetAnimatedEmoji#28703c8 = InputStickerSet; inputStickerSetDice#e67f520e emoticon:string = InputStickerSet; +inputStickerSetAnimatedEmojiAnimations#cde3739 = InputStickerSet; stickerSet#d7df217a flags:# archived:flags.1?true official:flags.2?true masks:flags.3?true animated:flags.5?true installed_date:flags.0?int id:long access_hash:long title:string short_name:string thumbs:flags.4?Vector thumb_dc_id:flags.4?int thumb_version:flags.4?int count:int hash:int = StickerSet; @@ -907,7 +908,6 @@ channelAdminLogEventActionExportedInviteRevoke#410a134e invite:ExportedChatInvit channelAdminLogEventActionExportedInviteEdit#e90ebb59 prev_invite:ExportedChatInvite new_invite:ExportedChatInvite = ChannelAdminLogEventAction; channelAdminLogEventActionParticipantVolume#3e7f6847 participant:GroupCallParticipant = ChannelAdminLogEventAction; channelAdminLogEventActionChangeHistoryTTL#6e941a38 prev_value:int new_value:int = ChannelAdminLogEventAction; -channelAdminLogEventActionChangeTheme#fe69018d prev_value:string new_value:string = ChannelAdminLogEventAction; channelAdminLogEvent#1fad68cd id:long date:int user_id:long action:ChannelAdminLogEventAction = ChannelAdminLogEvent; diff --git a/telethon_generator/data/errors.csv b/telethon_generator/data/errors.csv index ff9d8168..89546bc7 100644 --- a/telethon_generator/data/errors.csv +++ b/telethon_generator/data/errors.csv @@ -61,6 +61,7 @@ CHANNEL_BANNED,400,The channel is banned CHANNEL_INVALID,400,"Invalid channel object. Make sure to pass the right types, for instance making sure that the request is designed for channels or otherwise look for a different one more suited" CHANNEL_PRIVATE,400,The channel specified is private and you lack permission to access it. Another reason may be that you were banned from it CHANNEL_PUBLIC_GROUP_NA,403,channel/supergroup not available +CHANNEL_TOO_LARGE,406, CHAT_ABOUT_NOT_MODIFIED,400,About text has not changed CHAT_ABOUT_TOO_LONG,400,Chat about too long CHAT_ADMIN_INVITE_REQUIRED,403,You do not have the rights to do this @@ -177,6 +178,7 @@ INPUT_REQUEST_TOO_LONG,400,The input request was too long. This may be a bug in INPUT_USER_DEACTIVATED,400,The specified user was deleted INTERDC_X_CALL_ERROR,500,An error occurred while communicating with DC {dc} INTERDC_X_CALL_RICH_ERROR,500,A rich error occurred while communicating with DC {dc} +INVITE_FORBIDDEN_WITH_JOINAS,400, INVITE_HASH_EMPTY,400,The invite hash is empty INVITE_HASH_EXPIRED,400,The chat the user tried to join has expired and is not valid anymore INVITE_HASH_INVALID,400,The invite hash is invalid @@ -285,6 +287,7 @@ PUBLIC_KEY_REQUIRED,400, QUERY_ID_EMPTY,400,The query ID is empty QUERY_ID_INVALID,400,The query ID is invalid QUERY_TOO_SHORT,400,The query string is too short +QUIZ_ANSWER_MISSING,400, QUIZ_CORRECT_ANSWERS_EMPTY,400,A quiz must specify one correct answer QUIZ_CORRECT_ANSWERS_TOO_MUCH,400,There can only be one correct answer QUIZ_CORRECT_ANSWER_INVALID,400,The correct answer is not an existing answer @@ -376,6 +379,7 @@ USERNAME_OCCUPIED,400,The username is already taken USERS_TOO_FEW,400,"Not enough users (to create a chat, for example)" USERS_TOO_MUCH,400,"The maximum number of users has been exceeded (to create a chat, for example)" USER_ADMIN_INVALID,400,Either you're not an admin or you tried to ban an admin that you didn't promote +USER_ALREADY_INVITED,400, USER_ALREADY_PARTICIPANT,400,The authenticated user is already a participant of the chat USER_BANNED_IN_CHANNEL,400,You're banned from sending messages in supergroups/channels USER_BLOCKED,400,User blocked diff --git a/telethon_generator/data/methods.csv b/telethon_generator/data/methods.csv index 4427492b..79cbdc94 100644 --- a/telethon_generator/data/methods.csv +++ b/telethon_generator/data/methods.csv @@ -91,7 +91,7 @@ bots.setBotCommands,bot,BOT_COMMAND_DESCRIPTION_INVALID BOT_COMMAND_INVALID LANG channels.checkUsername,user,CHANNEL_INVALID CHAT_ID_INVALID USERNAME_INVALID channels.convertToGigagroup,user,PARTICIPANTS_TOO_FEW channels.createChannel,user,CHAT_TITLE_EMPTY USER_RESTRICTED -channels.deleteChannel,user,CHANNEL_INVALID CHANNEL_PRIVATE +channels.deleteChannel,user,CHANNEL_INVALID CHANNEL_PRIVATE CHANNEL_TOO_LARGE channels.deleteHistory,user, channels.deleteMessages,both,CHANNEL_INVALID CHANNEL_PRIVATE MESSAGE_DELETE_FORBIDDEN channels.deleteUserHistory,user,CHANNEL_INVALID CHAT_ADMIN_REQUIRED @@ -199,7 +199,7 @@ messages.editInlineBotMessage,both,MESSAGE_ID_INVALID MESSAGE_NOT_MODIFIED messages.editMessage,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_WRITE_FORBIDDEN INLINE_BOT_REQUIRED INPUT_USER_DEACTIVATED MEDIA_GROUPED_INVALID MEDIA_NEW_INVALID MEDIA_PREV_INVALID MESSAGE_AUTHOR_REQUIRED MESSAGE_EDIT_TIME_EXPIRED MESSAGE_EMPTY MESSAGE_ID_INVALID MESSAGE_NOT_MODIFIED PEER_ID_INVALID messages.exportChatInvite,both,CHAT_ID_INVALID messages.faveSticker,user,STICKER_ID_INVALID -messages.forwardMessages,both,BROADCAST_PUBLIC_VOTERS_FORBIDDEN CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_ID_INVALID CHAT_SEND_GIFS_FORBIDDEN CHAT_SEND_MEDIA_FORBIDDEN CHAT_SEND_STICKERS_FORBIDDEN CHAT_WRITE_FORBIDDEN GROUPED_MEDIA_INVALID INPUT_USER_DEACTIVATED MEDIA_EMPTY MESSAGE_IDS_EMPTY MESSAGE_ID_INVALID PEER_ID_INVALID PTS_CHANGE_EMPTY RANDOM_ID_DUPLICATE RANDOM_ID_INVALID SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH TIMEOUT USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT YOU_BLOCKED_USER +messages.forwardMessages,both,BROADCAST_PUBLIC_VOTERS_FORBIDDEN CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_ID_INVALID CHAT_SEND_GIFS_FORBIDDEN CHAT_SEND_MEDIA_FORBIDDEN CHAT_SEND_STICKERS_FORBIDDEN CHAT_WRITE_FORBIDDEN GROUPED_MEDIA_INVALID INPUT_USER_DEACTIVATED MEDIA_EMPTY MESSAGE_IDS_EMPTY MESSAGE_ID_INVALID PEER_ID_INVALID PTS_CHANGE_EMPTY QUIZ_ANSWER_MISSING RANDOM_ID_DUPLICATE RANDOM_ID_INVALID SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH TIMEOUT USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT YOU_BLOCKED_USER messages.getAllChats,user, messages.getAllDrafts,user, messages.getAllStickers,user, @@ -227,7 +227,7 @@ messages.getInlineGameHighScores,bot,MESSAGE_ID_INVALID USER_BOT_REQUIRED messages.getMaskStickers,user, messages.getMessageEditData,user,MESSAGE_AUTHOR_REQUIRED PEER_ID_INVALID messages.getMessages,both, -messages.getMessagesReadParticipants,user,CHAT_TOO_BIG +messages.getMessagesReadParticipants,user,CHAT_TOO_BIG MESSAGE_ID_INVALID messages.getMessagesViews,user,CHANNEL_PRIVATE CHAT_ID_INVALID PEER_ID_INVALID messages.getOnlines,user, messages.getPeerDialogs,user,CHANNEL_PRIVATE PEER_ID_INVALID @@ -288,7 +288,7 @@ messages.sendVote,user,MESSAGE_POLL_CLOSED OPTION_INVALID messages.setBotCallbackAnswer,both,QUERY_ID_INVALID URL_INVALID messages.setBotPrecheckoutResults,both,ERROR_TEXT_EMPTY messages.setBotShippingResults,both,QUERY_ID_INVALID -messages.setChatTheme,user,EMOJI_INVALID EMOJI_NOT_MODIFIED +messages.setChatTheme,user,EMOJI_INVALID EMOJI_NOT_MODIFIED PEER_ID_INVALID messages.setEncryptedTyping,user,CHAT_ID_INVALID messages.setGameScore,bot,PEER_ID_INVALID USER_BOT_REQUIRED messages.setHistoryTTL,user,CHAT_NOT_MODIFIED TTL_PERIOD_INVALID @@ -319,7 +319,7 @@ phone.discardCall,user,CALL_ALREADY_ACCEPTED CALL_PEER_INVALID phone.discardGroupCallRequest,user,GROUPCALL_ALREADY_DISCARDED phone.editGroupCallParticipant,user,USER_VOLUME_INVALID phone.getCallConfig,user, -phone.inviteToGroupCall,user,GROUPCALL_FORBIDDEN +phone.inviteToGroupCall,user,GROUPCALL_FORBIDDEN USER_ALREADY_INVITED INVITE_FORBIDDEN_WITH_JOINAS phone.joinGroupCall,user,GROUPCALL_ADD_PARTICIPANTS_FAILED GROUPCALL_SSRC_DUPLICATE_MUCH phone.joinGroupCallPresentation,user, PARTICIPANT_JOIN_MISSING phone.receivedCall,user,CALL_ALREADY_DECLINED CALL_PEER_INVALID @@ -342,11 +342,11 @@ stats.getMegagroupStats,user,CHAT_ADMIN_REQUIRED MEGAGROUP_REQUIRED STATS_MIGRAT stats.loadAsyncGraph,user,GRAPH_INVALID_RELOAD GRAPH_OUTDATED_RELOAD stickers.addStickerToSet,bot,BOT_MISSING STICKERSET_INVALID STICKER_PNG_NOPNG STICKER_TGS_NOTGS stickers.changeStickerPosition,bot,BOT_MISSING STICKER_INVALID -stickers.checkShortName,both,SHORT_NAME_INVALID SHORT_NAME_OCCUPIED +stickers.checkShortName,user,SHORT_NAME_INVALID SHORT_NAME_OCCUPIED stickers.createStickerSet,bot,BOT_MISSING PACK_SHORT_NAME_INVALID PACK_SHORT_NAME_OCCUPIED PEER_ID_INVALID SHORTNAME_OCCUPY_FAILED STICKERS_EMPTY STICKER_EMOJI_INVALID STICKER_FILE_INVALID STICKER_PNG_DIMENSIONS STICKER_PNG_NOPNG STICKER_TGS_NOTGS STICKER_THUMB_PNG_NOPNG STICKER_THUMB_TGS_NOTGS USER_ID_INVALID stickers.removeStickerFromSet,bot,BOT_MISSING STICKER_INVALID stickers.setStickerSetThumb,bot,STICKER_THUMB_PNG_NOPNG STICKER_THUMB_TGS_NOTGS -stickers.suggestShortName,both,TITLE_INVALID +stickers.suggestShortName,user,TITLE_INVALID updates.getChannelDifference,both,CHANNEL_INVALID CHANNEL_PRIVATE CHANNEL_PUBLIC_GROUP_NA HISTORY_GET_FAILED PERSISTENT_TIMESTAMP_EMPTY PERSISTENT_TIMESTAMP_INVALID PERSISTENT_TIMESTAMP_OUTDATED RANGES_INVALID TIMEOUT updates.getDifference,both,AUTH_KEY_PERM_EMPTY CDN_METHOD_INVALID DATE_EMPTY NEED_MEMBER_INVALID PERSISTENT_TIMESTAMP_EMPTY PERSISTENT_TIMESTAMP_INVALID SESSION_PASSWORD_NEEDED STORE_INVALID_SCALAR_TYPE TIMEOUT updates.getState,both,AUTH_KEY_DUPLICATED MSGID_DECREASE_RETRY SESSION_PASSWORD_NEEDED TIMEOUT From c9ecd61f7e4d3939040441886dc54f14ef7fea69 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 1 Dec 2021 19:03:38 +0100 Subject: [PATCH 110/256] Fix peer ID check to work with higher IDs This commit is taken from https://github.com/tulir/Telethon/commit/5f4bfe6b9be9da6d7ef0a55c67b7cc1cfd142fe2 --- telethon/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/telethon/utils.py b/telethon/utils.py index e8c59c01..7d058518 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -1029,13 +1029,13 @@ def get_peer_id(peer, add_mark=True): return peer.user_id elif isinstance(peer, types.PeerChat): # Check in case the user mixed things up to avoid blowing up - if not (0 < peer.chat_id <= 0x7fffffff): + if not (0 < peer.chat_id <= 9999999999): peer.chat_id = resolve_id(peer.chat_id)[0] return -peer.chat_id if add_mark else peer.chat_id else: # if isinstance(peer, types.PeerChannel): # Check in case the user mixed things up to avoid blowing up - if not (0 < peer.channel_id <= 0x7fffffff): + if not (0 < peer.channel_id <= 9999999999): peer.channel_id = resolve_id(peer.channel_id)[0] if not add_mark: From bda4259815406c36e331f2cb4889d3d2f70881e8 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 1 Dec 2021 19:08:47 +0100 Subject: [PATCH 111/256] Bump to v1.24 --- readthedocs/misc/changelog.rst | 24 ++++++++++++++++++++++++ telethon/version.py | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/readthedocs/misc/changelog.rst b/readthedocs/misc/changelog.rst index 951cf2e0..112aa117 100644 --- a/readthedocs/misc/changelog.rst +++ b/readthedocs/misc/changelog.rst @@ -13,6 +13,30 @@ it can take advantage of new goodies! .. contents:: List of All Versions +Rushed release to fix login (v1.24) +=================================== + ++------------------------+ +| Scheme layer used: 133 | ++------------------------+ + +This is a rushed release. It contains a layer recent enough to not fail with +``UPDATE_APP_TO_LOGIN``, but still not the latest, to avoid breaking more +than necessary. + +Breaking Changes +~~~~~~~~~~~~~~~~ + +* The biggest change is user identifiers (and chat identifiers, and others) + **now use up to 64 bits**, rather than 32. If you were storing them in some + storage with fixed size, you may need to update (such as database tables + storing only integers). + +There have been other changes which I currently don't have the time to document. +You can refer to the following link to see them early: +https://github.com/LonamiWebs/Telethon/compare/v1.23.0...v1.24.0 + + New schema and bug fixes (v1.23) ================================ diff --git a/telethon/version.py b/telethon/version.py index ab80aa1b..68e59657 100644 --- a/telethon/version.py +++ b/telethon/version.py @@ -1,3 +1,3 @@ # Versions should comply with PEP440. # This line is parsed in setup.py: -__version__ = '1.23.0' +__version__ = '1.24.0' From f9643bf7376a5953da2050a5361c9b465f7ee7d9 Mon Sep 17 00:00:00 2001 From: Reinier Romero Mir Date: Wed, 1 Dec 2021 14:28:55 -0500 Subject: [PATCH 112/256] Add missing async when downloading from URL (#3222) --- telethon/client/downloads.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/client/downloads.py b/telethon/client/downloads.py index 62ba6332..6d7a8d65 100644 --- a/telethon/client/downloads.py +++ b/telethon/client/downloads.py @@ -962,7 +962,7 @@ class DownloadMethods: f = file try: - with aiohttp.ClientSession() as session: + async with aiohttp.ClientSession() as session: # TODO Use progress_callback; get content length from response # https://github.com/telegramdesktop/tdesktop/blob/c7e773dd9aeba94e2be48c032edc9a78bb50234e/Telegram/SourceFiles/ui/images.cpp#L1318-L1319 async with session.get(web.url) as response: From dea424fdec421c97a7704db99087cc2aaa00ab84 Mon Sep 17 00:00:00 2001 From: penn5 Date: Sat, 11 Dec 2021 10:47:40 +0000 Subject: [PATCH 113/256] Fix typo in messages.py --- telethon/_client/messages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/_client/messages.py b/telethon/_client/messages.py index 4d2ad927..4a5ae371 100644 --- a/telethon/_client/messages.py +++ b/telethon/_client/messages.py @@ -488,7 +488,7 @@ async def send_message( else: request = _tl.fn.messages.SendMessage( peer=entity, - message=message, + message=message._text, entities=formatting_entities, no_webpage=not link_preview, reply_to_msg_id=utils.get_message_id(reply_to), From d3ef3c69c8ca460afdcc64ffde9a8890b3517dab Mon Sep 17 00:00:00 2001 From: penn5 Date: Sat, 11 Dec 2021 10:55:09 +0000 Subject: [PATCH 114/256] Remove _finish_init from newmessage.py This method was removed in 334a847de78f22dbbcf67f2e68c573d0d4b7c35d --- telethon/_events/newmessage.py | 1 - 1 file changed, 1 deletion(-) diff --git a/telethon/_events/newmessage.py b/telethon/_events/newmessage.py index 58e1a425..b7b9efc0 100644 --- a/telethon/_events/newmessage.py +++ b/telethon/_events/newmessage.py @@ -208,7 +208,6 @@ class NewMessage(EventBuilder): def _set_client(self, client): super()._set_client(client) m = self.message - m._finish_init(client, self._entities, None) self.__dict__['_init'] = True # No new attributes can be set def __getattr__(self, item): From 8d1379f3d4ada8de6b6fd84ac90ecf28bd088fb8 Mon Sep 17 00:00:00 2001 From: penn5 Date: Sat, 11 Dec 2021 10:56:49 +0000 Subject: [PATCH 115/256] Remove _finish_init from chataction.py This method was removed in 334a847de78f22dbbcf67f2e68c573d0d4b7c35d --- telethon/_events/chataction.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/telethon/_events/chataction.py b/telethon/_events/chataction.py index 75f19075..35a5d549 100644 --- a/telethon/_events/chataction.py +++ b/telethon/_events/chataction.py @@ -208,8 +208,6 @@ class ChatAction(EventBuilder): def _set_client(self, client): super()._set_client(client) - if self.action_message: - self.action_message._finish_init(client, self._entities, None) async def respond(self, *args, **kwargs): """ From 8de375323e871792732ecb26dd1db9c26976a110 Mon Sep 17 00:00:00 2001 From: Hackintosh 5 Date: Sat, 11 Dec 2021 12:31:38 +0000 Subject: [PATCH 116/256] Cleanup events code --- telethon/_client/updates.py | 3 ++- telethon/_events/album.py | 2 +- telethon/_events/callbackquery.py | 2 +- telethon/_events/chataction.py | 5 +---- telethon/_events/common.py | 3 ++- telethon/_events/inlinequery.py | 2 +- telethon/_events/messagedeleted.py | 2 +- telethon/_events/messageedited.py | 2 +- telethon/_events/messageread.py | 2 +- telethon/_events/newmessage.py | 14 +++++++------- telethon/_events/raw.py | 2 +- telethon/_events/userupdate.py | 2 +- 12 files changed, 20 insertions(+), 21 deletions(-) diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index 71a3e7e8..5f30fbd8 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -444,9 +444,10 @@ class EventBuilderDict: return self.__dict__[builder] except KeyError: event = self.__dict__[builder] = builder.build( - self.update, self.others, self.client._session_state.user_id) + self.update, self.others, self.client._session_state.user_id, self.entities or {}, self.client) if isinstance(event, EventCommon): + # TODO eww event.original_update = self.update event._entities = self.entities or {} event._set_client(self.client) diff --git a/telethon/_events/album.py b/telethon/_events/album.py index d8dacb98..cb9c39a8 100644 --- a/telethon/_events/album.py +++ b/telethon/_events/album.py @@ -96,7 +96,7 @@ class Album(EventBuilder): super().__init__(chats, blacklist_chats=blacklist_chats, func=func) @classmethod - def build(cls, update, others=None, self_id=None): + def build(cls, update, others=None, self_id=None, *todo, **todo2): if not others: return # We only care about albums which come inside the same Updates diff --git a/telethon/_events/callbackquery.py b/telethon/_events/callbackquery.py index 0c944400..15f8d12f 100644 --- a/telethon/_events/callbackquery.py +++ b/telethon/_events/callbackquery.py @@ -87,7 +87,7 @@ class CallbackQuery(EventBuilder): )) @classmethod - def build(cls, update, others=None, self_id=None): + def build(cls, update, others=None, self_id=None, *todo, **todo2): if isinstance(update, _tl.UpdateBotCallbackQuery): return cls.Event(update, update.peer, update.msg_id) elif isinstance(update, _tl.UpdateInlineBotCallbackQuery): diff --git a/telethon/_events/chataction.py b/telethon/_events/chataction.py index 35a5d549..b34ecd65 100644 --- a/telethon/_events/chataction.py +++ b/telethon/_events/chataction.py @@ -33,7 +33,7 @@ class ChatAction(EventBuilder): """ @classmethod - def build(cls, update, others=None, self_id=None): + def build(cls, update, others=None, self_id=None, *todo, **todo2): # Rely on specific pin updates for unpins, but otherwise ignore them # for new pins (we'd rather handle the new service message with pin, # so that we can act on that message'). @@ -206,9 +206,6 @@ class ChatAction(EventBuilder): self.new_score = new_score self.unpin = not pin - def _set_client(self, client): - super()._set_client(client) - async def respond(self, *args, **kwargs): """ Responds to the chat action message (not as a reply). Shorthand for diff --git a/telethon/_events/common.py b/telethon/_events/common.py index f7b2e066..bfaf3227 100644 --- a/telethon/_events/common.py +++ b/telethon/_events/common.py @@ -67,7 +67,7 @@ class EventBuilder(abc.ABC): @classmethod @abc.abstractmethod - def build(cls, update, others=None, self_id=None): + def build(cls, update, others, self_id, entities, client): """ Builds an event for the given update if possible, or returns None. @@ -144,6 +144,7 @@ class EventCommon(ChatGetter, abc.ABC): """ Setter so subclasses can act accordingly when the client is set. """ + # TODO Nuke self._client = client if self._chat_peer: self._chat, self._input_chat = utils._get_entity_pair( diff --git a/telethon/_events/inlinequery.py b/telethon/_events/inlinequery.py index 0af44dd0..d3cd6822 100644 --- a/telethon/_events/inlinequery.py +++ b/telethon/_events/inlinequery.py @@ -61,7 +61,7 @@ class InlineQuery(EventBuilder): raise TypeError('Invalid pattern type given') @classmethod - def build(cls, update, others=None, self_id=None): + def build(cls, update, others=None, self_id=None, *todo, **todo2): if isinstance(update, _tl.UpdateBotInlineQuery): return cls.Event(update) diff --git a/telethon/_events/messagedeleted.py b/telethon/_events/messagedeleted.py index f2a2e9f9..58f9ff5f 100644 --- a/telethon/_events/messagedeleted.py +++ b/telethon/_events/messagedeleted.py @@ -36,7 +36,7 @@ class MessageDeleted(EventBuilder): print('Message', msg_id, 'was deleted in', event.chat_id) """ @classmethod - def build(cls, update, others=None, self_id=None): + def build(cls, update, others=None, self_id=None, *todo, **todo2): if isinstance(update, _tl.UpdateDeleteMessages): return cls.Event( deleted_ids=update.messages, diff --git a/telethon/_events/messageedited.py b/telethon/_events/messageedited.py index 8eefb9f5..3f430a68 100644 --- a/telethon/_events/messageedited.py +++ b/telethon/_events/messageedited.py @@ -43,7 +43,7 @@ class MessageEdited(NewMessage): print('Message', event.id, 'changed at', event.date) """ @classmethod - def build(cls, update, others=None, self_id=None): + def build(cls, update, others=None, self_id=None, *todo, **todo2): if isinstance(update, (_tl.UpdateEditMessage, _tl.UpdateEditChannelMessage)): return cls.Event(update.message) diff --git a/telethon/_events/messageread.py b/telethon/_events/messageread.py index 5c37eb2c..0cd50de0 100644 --- a/telethon/_events/messageread.py +++ b/telethon/_events/messageread.py @@ -35,7 +35,7 @@ class MessageRead(EventBuilder): self.inbox = inbox @classmethod - def build(cls, update, others=None, self_id=None): + def build(cls, update, others=None, self_id=None, *todo, **todo2): if isinstance(update, _tl.UpdateReadHistoryInbox): return cls.Event(update.peer, update.max_id, False) elif isinstance(update, _tl.UpdateReadHistoryOutbox): diff --git a/telethon/_events/newmessage.py b/telethon/_events/newmessage.py index b7b9efc0..e4887002 100644 --- a/telethon/_events/newmessage.py +++ b/telethon/_events/newmessage.py @@ -95,14 +95,14 @@ class NewMessage(EventBuilder): self.from_users = await _into_id_set(client, self.from_users) @classmethod - def build(cls, update, others=None, self_id=None): + def build(cls, update, others, self_id, entities, client): if isinstance(update, (_tl.UpdateNewMessage, _tl.UpdateNewChannelMessage)): if not isinstance(update.message, _tl.Message): return # We don't care about MessageService's here - event = cls.Event(update.message) + msg = update.message elif isinstance(update, _tl.UpdateShortMessage): - event = cls.Event(_tl.Message( + msg = _tl.Message( out=update.out, mentioned=update.mentioned, media_unread=update.media_unread, @@ -117,9 +117,9 @@ class NewMessage(EventBuilder): reply_to=update.reply_to, entities=update.entities, ttl_period=update.ttl_period - )) + ) elif isinstance(update, _tl.UpdateShortChatMessage): - event = cls.Event(_tl.Message( + msg = _tl.Message( out=update.out, mentioned=update.mentioned, media_unread=update.media_unread, @@ -134,11 +134,11 @@ class NewMessage(EventBuilder): reply_to=update.reply_to, entities=update.entities, ttl_period=update.ttl_period - )) + ) else: return - return event + return cls.Event(_custom.Message._new(client, msg, entities, None)) def filter(self, event): if self._no_check: diff --git a/telethon/_events/raw.py b/telethon/_events/raw.py index 68fdfc0c..496f39e5 100644 --- a/telethon/_events/raw.py +++ b/telethon/_events/raw.py @@ -42,7 +42,7 @@ class Raw(EventBuilder): self.resolved = True @classmethod - def build(cls, update, others=None, self_id=None): + def build(cls, update, others=None, self_id=None, *todo, **todo2): return update def filter(self, event): diff --git a/telethon/_events/userupdate.py b/telethon/_events/userupdate.py index 61db39ea..b5354ae0 100644 --- a/telethon/_events/userupdate.py +++ b/telethon/_events/userupdate.py @@ -49,7 +49,7 @@ class UserUpdate(EventBuilder): await client.send_message(event.user_id, 'What are you sending?') """ @classmethod - def build(cls, update, others=None, self_id=None): + def build(cls, update, others=None, self_id=None, *todo, **todo2): if isinstance(update, _tl.UpdateUserStatus): return cls.Event(_tl.PeerUser(update.user_id), status=update.status) From b566e59036f91739961a0f2e5bb200d32ac03d16 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 16 Oct 2021 11:53:29 +0200 Subject: [PATCH 117/256] Add stringify back to custom Message --- readthedocs/misc/v2-migration-guide.rst | 33 +++++++++++ telethon/_misc/helpers.py | 67 ++++++++++++++++++++++ telethon/_misc/tlobject.py | 75 ++----------------------- telethon/types/_custom/message.py | 47 +++++++++++++++- 4 files changed, 152 insertions(+), 70 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index c16ed02f..0b8bbc22 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -526,6 +526,39 @@ If you still want the old behaviour, wrap the list inside another list: #+ +Changes to the string and to_dict representation +------------------------------------------------ + +The string representation of raw API objects will now have its "printing depth" limited, meaning +very large and nested objects will be easier to read. + +If you want to see the full object's representation, you should instead use Python's builtin +``repr`` method. + +The ``.stringify`` method remains unchanged. + +Here's a comparison table for a convenient overview: + ++-------------------+---------------------------------------------+---------------------------------------------+ +| | Telethon v1.x | Telethon v2.x | ++-------------------+-------------+--------------+----------------+-------------+--------------+----------------+ +| | ``__str__`` | ``__repr__`` | ``.stringify`` | ``__str__`` | ``__repr__`` | ``.stringify`` | ++-------------------+-------------+--------------+----------------+-------------+--------------+----------------+ +| Useful? | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ++-------------------+-------------+--------------+----------------+-------------+--------------+----------------+ +| Multiline? | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | ++-------------------+-------------+--------------+----------------+-------------+--------------+----------------+ +| Shows everything? | ✅ | ❌ | ✅ | ❌ | ✅ | ✅ | ++-------------------+-------------+--------------+----------------+-------------+--------------+----------------+ + +Both of the string representations may still change in the future without warning, as Telegram +adds, changes or removes fields. It should only be used for debugging. If you need a persistent +string representation, it is your job to decide which fields you care about and their format. + +The ``Message`` representation now contains different properties, which should be more useful and +less confusing. + + Changes on how to configure a different connection mode ------------------------------------------------------- diff --git a/telethon/_misc/helpers.py b/telethon/_misc/helpers.py index f9297816..a3480007 100644 --- a/telethon/_misc/helpers.py +++ b/telethon/_misc/helpers.py @@ -200,6 +200,73 @@ def _entity_type(entity): # 'Empty' in name or not found, we don't care, not a valid entity. raise TypeError('{} does not have any entity type'.format(entity)) + +def pretty_print(obj, indent=None, max_depth=float('inf')): + max_depth -= 1 + if max_depth < 0: + return '...' + + to_d = getattr(obj, '_to_dict', None) or getattr(obj, 'to_dict', None) + if callable(to_d): + obj = to_d() + + if indent is None: + if isinstance(obj, dict): + return '{}({})'.format(obj.get('_', 'dict'), ', '.join( + '{}={}'.format(k, pretty_print(v, indent, max_depth)) + for k, v in obj.items() if k != '_' + )) + elif isinstance(obj, str) or isinstance(obj, bytes): + return repr(obj) + elif hasattr(obj, '__iter__'): + return '[{}]'.format( + ', '.join(pretty_print(x, indent, max_depth) for x in obj) + ) + else: + return repr(obj) + else: + result = [] + + if isinstance(obj, dict): + result.append(obj.get('_', 'dict')) + result.append('(') + if obj: + result.append('\n') + indent += 1 + for k, v in obj.items(): + if k == '_': + continue + result.append('\t' * indent) + result.append(k) + result.append('=') + result.append(pretty_print(v, indent, max_depth)) + result.append(',\n') + result.pop() # last ',\n' + indent -= 1 + result.append('\n') + result.append('\t' * indent) + result.append(')') + + elif isinstance(obj, str) or isinstance(obj, bytes): + result.append(repr(obj)) + + elif hasattr(obj, '__iter__'): + result.append('[\n') + indent += 1 + for x in obj: + result.append('\t' * indent) + result.append(pretty_print(x, indent, max_depth)) + result.append(',\n') + indent -= 1 + result.append('\t' * indent) + result.append(']') + + else: + result.append(repr(obj)) + + return ''.join(result) + + # endregion # region Cryptographic related utils diff --git a/telethon/_misc/tlobject.py b/telethon/_misc/tlobject.py index da2ed6d6..4045e3ed 100644 --- a/telethon/_misc/tlobject.py +++ b/telethon/_misc/tlobject.py @@ -3,6 +3,7 @@ import json import struct from datetime import datetime, date, timedelta, timezone import time +from .helpers import pretty_print _EPOCH_NAIVE = datetime(*time.gmtime(0)[:6]) _EPOCH_NAIVE_LOCAL = datetime(*time.localtime(0)[:6]) @@ -36,73 +37,6 @@ class TLObject: CONSTRUCTOR_ID = None SUBCLASS_OF_ID = None - @staticmethod - def pretty_format(obj, indent=None): - """ - Pretty formats the given object as a string which is returned. - If indent is None, a single line will be returned. - """ - if indent is None: - if isinstance(obj, TLObject): - obj = obj.to_dict() - - if isinstance(obj, dict): - return '{}({})'.format(obj.get('_', 'dict'), ', '.join( - '{}={}'.format(k, TLObject.pretty_format(v)) - for k, v in obj.items() if k != '_' - )) - elif isinstance(obj, str) or isinstance(obj, bytes): - return repr(obj) - elif hasattr(obj, '__iter__'): - return '[{}]'.format( - ', '.join(TLObject.pretty_format(x) for x in obj) - ) - else: - return repr(obj) - else: - result = [] - if isinstance(obj, TLObject): - obj = obj.to_dict() - - if isinstance(obj, dict): - result.append(obj.get('_', 'dict')) - result.append('(') - if obj: - result.append('\n') - indent += 1 - for k, v in obj.items(): - if k == '_': - continue - result.append('\t' * indent) - result.append(k) - result.append('=') - result.append(TLObject.pretty_format(v, indent)) - result.append(',\n') - result.pop() # last ',\n' - indent -= 1 - result.append('\n') - result.append('\t' * indent) - result.append(')') - - elif isinstance(obj, str) or isinstance(obj, bytes): - result.append(repr(obj)) - - elif hasattr(obj, '__iter__'): - result.append('[\n') - indent += 1 - for x in obj: - result.append('\t' * indent) - result.append(TLObject.pretty_format(x, indent)) - result.append(',\n') - indent -= 1 - result.append('\t' * indent) - result.append(']') - - else: - result.append(repr(obj)) - - return ''.join(result) - @staticmethod def serialize_bytes(data): """Write bytes by using Telegram guidelines""" @@ -164,11 +98,14 @@ class TLObject: def __ne__(self, o): return not isinstance(o, type(self)) or self.to_dict() != o.to_dict() + def __repr__(self): + return pretty_print(self) + def __str__(self): - return TLObject.pretty_format(self) + return pretty_print(self, max_depth=2) def stringify(self): - return TLObject.pretty_format(self, indent=0) + return pretty_print(self, indent=0) def to_dict(self): res = {} diff --git a/telethon/types/_custom/message.py b/telethon/types/_custom/message.py index 103da65b..bd0b61ee 100644 --- a/telethon/types/_custom/message.py +++ b/telethon/types/_custom/message.py @@ -9,7 +9,7 @@ from .file import File from .inputfile import InputFile from .inputmessage import InputMessage from .button import build_reply_markup -from ..._misc import utils, tlobject +from ..._misc import utils, helpers, tlobject from ... import _tl, _misc @@ -1366,5 +1366,50 @@ class Message(ChatGetter, SenderGetter): # endregion Private Methods + def to_dict(self): + return self._message.to_dict() + + def _to_dict(self): + return { + '_': 'Message', + 'id': self.id, + 'out': self.out, + 'date': self.date, + 'text': self.text, + 'sender': self.sender, + 'chat': self.chat, + 'mentioned': self.mentioned, + 'media_unread': self.media_unread, + 'silent': self.silent, + 'post': self.post, + 'from_scheduled': self.from_scheduled, + 'legacy': self.legacy, + 'edit_hide': self.edit_hide, + 'pinned': self.pinned, + 'forward': self.forward, + 'via_bot': self.via_bot, + 'reply_to': self.reply_to, + 'reply_markup': self.reply_markup, + 'views': self.views, + 'forwards': self.forwards, + 'replies': self.replies, + 'edit_date': self.edit_date, + 'post_author': self.post_author, + 'grouped_id': self.grouped_id, + 'ttl_period': self.ttl_period, + 'action': self.action, + 'media': self.media, + 'action_entities': self.action_entities, + } + + def __repr__(self): + return helpers.pretty_print(self) + + def __str__(self): + return helpers.pretty_print(self, max_depth=2) + + def stringify(self): + return helpers.pretty_print(self, indent=0) + # TODO set md by default if commonmark is installed else nothing From dbe66bf805de4c0baec73979a8c80ea654fb323d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 16 Oct 2021 11:55:18 +0200 Subject: [PATCH 118/256] Remove TLObject.to_json --- readthedocs/misc/v2-migration-guide.rst | 8 ++++++++ telethon/_misc/tlobject.py | 17 ----------------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 0b8bbc22..c328549c 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -589,6 +589,14 @@ broken for some time now (see `issue #1319 Date: Sat, 16 Oct 2021 12:40:25 +0200 Subject: [PATCH 119/256] Add enum for typing action --- readthedocs/misc/changelog.rst | 12 +++++++ telethon/_client/chats.py | 60 +++++++++++++--------------------- telethon/_misc/enums.py | 22 +++++++++++++ telethon/enums.py | 1 + 4 files changed, 58 insertions(+), 37 deletions(-) diff --git a/readthedocs/misc/changelog.rst b/readthedocs/misc/changelog.rst index 951cf2e0..74005356 100644 --- a/readthedocs/misc/changelog.rst +++ b/readthedocs/misc/changelog.rst @@ -13,6 +13,18 @@ it can take advantage of new goodies! .. contents:: List of All Versions + +Complete overhaul of the library (v2.0) +======================================= + ++------------------------+ +| Scheme layer used: 133 | ++------------------------+ + +(inc and link all of migration guide) +properly-typed enums for filters and actions + + New schema and bug fixes (v1.23) ================================ diff --git a/telethon/_client/chats.py b/telethon/_client/chats.py index 693feeab..3034b5d9 100644 --- a/telethon/_client/chats.py +++ b/telethon/_client/chats.py @@ -17,31 +17,6 @@ _MAX_PROFILE_PHOTO_CHUNK_SIZE = 100 class _ChatAction: - _str_mapping = { - 'typing': _tl.SendMessageTypingAction(), - 'contact': _tl.SendMessageChooseContactAction(), - 'game': _tl.SendMessageGamePlayAction(), - 'location': _tl.SendMessageGeoLocationAction(), - 'sticker': _tl.SendMessageChooseStickerAction(), - - 'record-audio': _tl.SendMessageRecordAudioAction(), - 'record-voice': _tl.SendMessageRecordAudioAction(), # alias - 'record-round': _tl.SendMessageRecordRoundAction(), - 'record-video': _tl.SendMessageRecordVideoAction(), - - 'audio': _tl.SendMessageUploadAudioAction(1), - 'voice': _tl.SendMessageUploadAudioAction(1), # alias - 'song': _tl.SendMessageUploadAudioAction(1), # alias - 'round': _tl.SendMessageUploadRoundAction(1), - 'video': _tl.SendMessageUploadVideoAction(1), - - 'photo': _tl.SendMessageUploadPhotoAction(1), - 'document': _tl.SendMessageUploadDocumentAction(1), - 'file': _tl.SendMessageUploadDocumentAction(1), # alias - - 'cancel': _tl.SendMessageCancelAction() - } - def __init__(self, client, chat, action, *, delay, auto_cancel): self._client = client self._chat = chat @@ -88,6 +63,28 @@ class _ChatAction: await self._client(_tl.fn.messages.SetTyping( self._chat, _tl.SendMessageCancelAction())) + @staticmethod + def _parse(action): + if isinstance(action, tlobject.TLObject) and action.SUBCLASS_OF_ID != 0x20b2cc21: + return action + + return { + enums.TYPING: _tl.SendMessageTypingAction(), + enums.CONTACT: _tl.SendMessageChooseContactAction(), + enums.GAME: _tl.SendMessageGamePlayAction(), + enums.LOCATION: _tl.SendMessageGeoLocationAction(), + enums.STICKER: _tl.SendMessageChooseStickerAction(), + enums.RECORD_AUDIO: _tl.SendMessageRecordAudioAction(), + enums.RECORD_ROUND: _tl.SendMessageRecordRoundAction(), + enums.RECORD_VIDEO: _tl.SendMessageRecordVideoAction(), + enums.AUDIO: _tl.SendMessageUploadAudioAction(1), + enums.ROUND: _tl.SendMessageUploadRoundAction(1), + enums.VIDEO: _tl.SendMessageUploadVideoAction(1), + enums.PHOTO: _tl.SendMessageUploadPhotoAction(1), + enums.DOCUMENT: _tl.SendMessageUploadDocumentAction(1), + enums.CANCEL: _tl.SendMessageCancelAction(), + }[enums.parse_typing_action(action)] + def progress(self, current, total): if hasattr(self._action, 'progress'): self._action.progress = 100 * round(current / total) @@ -457,18 +454,7 @@ def action( *, delay: float = 4, auto_cancel: bool = True) -> 'typing.Union[_ChatAction, typing.Coroutine]': - if isinstance(action, str): - try: - action = _ChatAction._str_mapping[action.lower()] - except KeyError: - raise ValueError( - 'No such action "{}"'.format(action)) from None - elif not isinstance(action, tlobject.TLObject) or action.SUBCLASS_OF_ID != 0x20b2cc21: - # 0x20b2cc21 = crc32(b'SendMessageAction') - if isinstance(action, type): - raise ValueError('You must pass an instance, not the class') - else: - raise ValueError('Cannot use {} as action'.format(action)) + action = _ChatAction._parse(action) if isinstance(action, _tl.SendMessageCancelAction): # ``SetTyping.resolve`` will get input peer of ``entity``. diff --git a/telethon/_misc/enums.py b/telethon/_misc/enums.py index edce6776..c8fa656b 100644 --- a/telethon/_misc/enums.py +++ b/telethon/_misc/enums.py @@ -17,6 +17,27 @@ class Participant(Enum): CONTACT = 'contact' +class Action(Enum): + TYPING = 'typing' + CONTACT = 'contact' + GAME = 'game' + LOCATION = 'location' + STICKER = 'sticker' + RECORD_AUDIO = 'record-audio' + RECORD_VOICE = RECORD_AUDIO + RECORD_ROUND = 'record-round' + RECORD_VIDEO = 'record-video' + AUDIO = 'audio' + VOICE = AUDIO + SONG = AUDIO + ROUND = 'round' + VIDEO = 'video' + PHOTO = 'photo' + DOCUMENT = 'document' + FILE = DOCUMENT + CANCEL = 'cancel' + + def _mk_parser(cls): def parser(value): if isinstance(value, cls): @@ -35,3 +56,4 @@ def _mk_parser(cls): parse_conn_mode = _mk_parser(ConnectionMode) parse_participant = _mk_parser(Participant) +parse_typing_action = _mk_parser(Action) diff --git a/telethon/enums.py b/telethon/enums.py index 8de39a15..bad39ea0 100644 --- a/telethon/enums.py +++ b/telethon/enums.py @@ -1,4 +1,5 @@ from ._misc.enums import ( ConnectionMode, Participant, + Action, ) From e2132d5f7c328388424ab3626e32137f5b7b0375 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 16 Oct 2021 13:56:38 +0200 Subject: [PATCH 120/256] Change the way thumb size selection works --- readthedocs/misc/v2-migration-guide.rst | 6 ++ telethon/_client/downloads.py | 63 +++++----------- telethon/_client/telegramclient.py | 45 +++++------- telethon/_misc/enums.py | 96 +++++++++++++++++++++++++ telethon/enums.py | 1 + 5 files changed, 139 insertions(+), 72 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index c328549c..e4e111a4 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -706,3 +706,9 @@ formatting_entities stays because otherwise it's the only feasible way to manual todo update send_message and send_file docs (well review all functions) album overhaul. use a list of Message instead. + +size selector for download_profile_photo and download_media is now different + +still thumb because otherwise documents are weird. + +keep support for explicit size instance? diff --git a/telethon/_client/downloads.py b/telethon/_client/downloads.py index b29206d4..6339b8c9 100644 --- a/telethon/_client/downloads.py +++ b/telethon/_client/downloads.py @@ -7,7 +7,7 @@ import inspect import asyncio from .._crypto import AES -from .._misc import utils, helpers, requestiter, tlobject, hints +from .._misc import utils, helpers, requestiter, tlobject, hints, enums from .. import errors, _tl try: @@ -180,7 +180,7 @@ async def download_profile_photo( entity: 'hints.EntityLike', file: 'hints.FileLike' = None, *, - download_big: bool = True) -> typing.Optional[str]: + thumb) -> typing.Optional[str]: # hex(crc32(x.encode('ascii'))) for x in # ('User', 'Chat', 'UserFull', 'ChatFull') ENTITIES = (0x2da17977, 0xc5af5d94, 0x1f4661b9, 0xd49a2697) @@ -189,8 +189,6 @@ async def download_profile_photo( if not isinstance(entity, tlobject.TLObject) or entity.SUBCLASS_OF_ID in INPUTS: entity = await self.get_entity(entity) - thumb = -1 if download_big else 0 - possible_names = [] if entity.SUBCLASS_OF_ID not in ENTITIES: photo = entity @@ -212,11 +210,13 @@ async def download_profile_photo( photo = entity.photo if isinstance(photo, (_tl.UserProfilePhoto, _tl.ChatPhoto)): + thumb = enums.Size.ORIGINAL if thumb == () else enums.parse_photo_size(thumb) + dc_id = photo.dc_id loc = _tl.InputPeerPhotoFileLocation( peer=await self.get_input_entity(entity), photo_id=photo.photo_id, - big=download_big + big=thumb >= enums.Size.LARGE ) else: # It doesn't make any sense to check if `photo` can be used @@ -259,7 +259,7 @@ async def download_media( message: 'hints.MessageLike', file: 'hints.FileLike' = None, *, - thumb: 'typing.Union[int, _tl.TypePhotoSize]' = None, + size = (), progress_callback: 'hints.ProgressCallback' = None) -> typing.Optional[typing.Union[str, bytes]]: # Downloading large documents may be slow enough to require a new file reference # to be obtained mid-download. Store (input chat, message id) so that the message @@ -292,11 +292,11 @@ async def download_media( return await _download_document( self, media, file, date, thumb, progress_callback, msg_data ) - elif isinstance(media, _tl.MessageMediaContact) and thumb is None: + elif isinstance(media, _tl.MessageMediaContact): return _download_contact( self, media, file ) - elif isinstance(media, (_tl.WebDocument, _tl.WebDocumentNoProxy)) and thumb is None: + elif isinstance(media, (_tl.WebDocument, _tl.WebDocumentNoProxy)): return await _download_web_document( self, media, file, progress_callback ) @@ -491,44 +491,15 @@ def _iter_download( def _get_thumb(thumbs, thumb): - # Seems Telegram has changed the order and put `PhotoStrippedSize` - # last while this is the smallest (layer 116). Ensure we have the - # sizes sorted correctly with a custom function. - def sort_thumbs(thumb): - if isinstance(thumb, _tl.PhotoStrippedSize): - return 1, len(thumb.bytes) - if isinstance(thumb, _tl.PhotoCachedSize): - return 1, len(thumb.bytes) - if isinstance(thumb, _tl.PhotoSize): - return 1, thumb.size - if isinstance(thumb, _tl.PhotoSizeProgressive): - return 1, max(thumb.sizes) - if isinstance(thumb, _tl.VideoSize): - return 2, thumb.size - - # Empty size or invalid should go last - return 0, 0 - - thumbs = list(sorted(thumbs, key=sort_thumbs)) - - for i in reversed(range(len(thumbs))): - # :tl:`PhotoPathSize` is used for animated stickers preview, and the thumb is actually - # a SVG path of the outline. Users expect thumbnails to be JPEG files, so pretend this - # thumb size doesn't actually exist (#1655). - if isinstance(thumbs[i], _tl.PhotoPathSize): - thumbs.pop(i) - - if thumb is None: - return thumbs[-1] - elif isinstance(thumb, int): - return thumbs[thumb] - elif isinstance(thumb, str): - return next((t for t in thumbs if t.type == thumb), None) - elif isinstance(thumb, (_tl.PhotoSize, _tl.PhotoCachedSize, - _tl.PhotoStrippedSize, _tl.VideoSize)): + if isinstance(thumb, tlobject.TLObject): return thumb - else: - return None + + thumb = enums.parse_photo_size(thumb) + return min( + thumbs, + default=None, + key=lambda t: abs(thumb - enums.parse_photo_size(t.type)) + ) def _download_cached_photo_size(self: 'TelegramClient', size, file): # No need to download anything, simply write the bytes @@ -623,7 +594,7 @@ async def _download_document( if not isinstance(document, _tl.Document): return - if thumb is None: + if thumb == (): kind, possible_names = _get_kind_and_names(document.attributes) file = _get_proper_filename( file, kind, utils.get_extension(document), diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index f2dc4ede..034b0a30 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -1610,7 +1610,7 @@ class TelegramClient: entity: 'hints.EntityLike', file: 'hints.FileLike' = None, *, - download_big: bool = True) -> typing.Optional[str]: + thumb: typing.Union[str, enums.Size] = ()) -> typing.Optional[str]: """ Downloads the profile photo from the given user, chat or channel. @@ -1634,8 +1634,15 @@ class TelegramClient: If file is the type `bytes`, it will be downloaded in-memory as a bytestring (e.g. ``file=bytes``). - download_big (`bool`, optional): - Whether to use the big version of the available photos. + thumb (optional): + The thumbnail size to download. A different size may be chosen + if the specified size doesn't exist. The category of the size + you choose will be respected when possible (e.g. if you + specify a cropped size, a cropped variant of similar size will + be preferred over a boxed variant of similar size). Cropped + images are considered to be smaller than boxed images. + + By default, the largest size (original) is downloaded. Returns `None` if no photo was provided, or if it was Empty. On success @@ -1655,7 +1662,7 @@ class TelegramClient: message: 'hints.MessageLike', file: 'hints.FileLike' = None, *, - thumb: 'typing.Union[int, _tl.TypePhotoSize]' = None, + thumb: typing.Union[str, enums.Size] = (), progress_callback: 'hints.ProgressCallback' = None) -> typing.Optional[typing.Union[str, bytes]]: """ Downloads the given media from a message object. @@ -1680,29 +1687,15 @@ class TelegramClient: A callback function accepting two parameters: ``(received bytes, total)``. - thumb (`int` | :tl:`PhotoSize`, optional): - Which thumbnail size from the document or photo to download, - instead of downloading the document or photo itself. + thumb (optional): + The thumbnail size to download. A different size may be chosen + if the specified size doesn't exist. The category of the size + you choose will be respected when possible (e.g. if you + specify a cropped size, a cropped variant of similar size will + be preferred over a boxed variant of similar size). Cropped + images are considered to be smaller than boxed images. - If it's specified but the file does not have a thumbnail, - this method will return `None`. - - The parameter should be an integer index between ``0`` and - ``len(sizes)``. ``0`` will download the smallest thumbnail, - and ``len(sizes) - 1`` will download the largest thumbnail. - You can also use negative indices, which work the same as - they do in Python's `list`. - - You can also pass the :tl:`PhotoSize` instance to use. - Alternatively, the thumb size type `str` may be used. - - In short, use ``thumb=0`` if you want the smallest thumbnail - and ``thumb=-1`` if you want the largest thumbnail. - - .. note:: - The largest thumbnail may be a video instead of a photo, - as they are available since layer 116 and are bigger than - any of the photos. + By default, the original media is downloaded. Returns `None` if no media was provided, or if it was Empty. On success diff --git a/telethon/_misc/enums.py b/telethon/_misc/enums.py index c8fa656b..2d5742aa 100644 --- a/telethon/_misc/enums.py +++ b/telethon/_misc/enums.py @@ -1,6 +1,16 @@ from enum import Enum +def _impl_op(which): + def op(self, other): + if not isinstance(other, type(self)): + return NotImplemented + + return getattr(self._val(), which)(other._val()) + + return op + + class ConnectionMode(Enum): FULL = 'full' INTERMEDIATE = 'intermediate' @@ -38,6 +48,91 @@ class Action(Enum): CANCEL = 'cancel' +class Size(Enum): + """ + See https://core.telegram.org/api/files#image-thumbnail-types. + + * ``'s'``. The image fits within a box of 100x100. + * ``'m'``. The image fits within a box of 320x320. + * ``'x'``. The image fits within a box of 800x800. + * ``'y'``. The image fits within a box of 1280x1280. + * ``'w'``. The image fits within a box of 2560x2560. + * ``'a'``. The image was cropped to be at most 160x160. + * ``'b'``. The image was cropped to be at most 320x320. + * ``'c'``. The image was cropped to be at most 640x640. + * ``'d'``. The image was cropped to be at most 1280x1280. + * ``'i'``. The image comes inline (no need to download anything). + * ``'j'``. Only the image outline is present (for stickers). + * ``'u'``. The image is actually a short MPEG4 animated video. + * ``'v'``. The image is actually a short MPEG4 video preview. + + The sorting order is first dimensions, then ``cropped < boxed < video < other``. + """ + SMALL = 's' + MEDIUM = 'm' + LARGE = 'x' + EXTRA_LARGE = 'y' + ORIGINAL = 'w' + CROPPED_SMALL = 'a' + CROPPED_MEDIUM = 'b' + CROPPED_LARGE = 'c' + CROPPED_EXTRA_LARGE = 'd' + INLINE = 'i' + OUTLINE = 'j' + ANIMATED = 'u' + VIDEO = 'v' + + def __hash__(self): + return object.__hash__(self) + + __sub__ = _impl_op('__sub__') + __lt__ = _impl_op('__lt__') + __le__ = _impl_op('__le__') + __eq__ = _impl_op('__eq__') + __ne__ = _impl_op('__ne__') + __gt__ = _impl_op('__gt__') + __ge__ = _impl_op('__ge__') + + def _val(self): + return self._category() * 100 + self._size() + + def _category(self): + return { + Size.SMALL: 2, + Size.MEDIUM: 2, + Size.LARGE: 2, + Size.EXTRA_LARGE: 2, + Size.ORIGINAL: 2, + Size.CROPPED_SMALL: 1, + Size.CROPPED_MEDIUM: 1, + Size.CROPPED_LARGE: 1, + Size.CROPPED_EXTRA_LARGE: 1, + Size.INLINE: 4, + Size.OUTLINE: 5, + Size.ANIMATED: 3, + Size.VIDEO: 3, + }[self] + + def _size(self): + return { + Size.SMALL: 1, + Size.MEDIUM: 3, + Size.LARGE: 5, + Size.EXTRA_LARGE: 6, + Size.ORIGINAL: 7, + Size.CROPPED_SMALL: 2, + Size.CROPPED_MEDIUM: 3, + Size.CROPPED_LARGE: 4, + Size.CROPPED_EXTRA_LARGE: 6, + # 0, since they're not the original photo at all + Size.INLINE: 0, + Size.OUTLINE: 0, + # same size as original or extra large (videos are large) + Size.ANIMATED: 7, + Size.VIDEO: 6, + }[self] + + def _mk_parser(cls): def parser(value): if isinstance(value, cls): @@ -57,3 +152,4 @@ def _mk_parser(cls): parse_conn_mode = _mk_parser(ConnectionMode) parse_participant = _mk_parser(Participant) parse_typing_action = _mk_parser(Action) +parse_photo_size = _mk_parser(Size) diff --git a/telethon/enums.py b/telethon/enums.py index bad39ea0..ef7715cc 100644 --- a/telethon/enums.py +++ b/telethon/enums.py @@ -2,4 +2,5 @@ from ._misc.enums import ( ConnectionMode, Participant, Action, + Size, ) From 1b15a34f6928dfb711d4a2056507026f8ec333d6 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 16 Oct 2021 13:59:33 +0200 Subject: [PATCH 121/256] Remove parse_mode from the client --- readthedocs/misc/v2-migration-guide.rst | 8 +++-- telethon/_client/messageparse.py | 10 ------ telethon/_client/telegramclient.py | 44 ------------------------- 3 files changed, 6 insertions(+), 56 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index e4e111a4..0b6a7c01 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -207,8 +207,10 @@ The ``telethon.errors`` module continues to provide custom errors used by the li // TODO provide a way to see which errors are known in the docs or at tl.telethon.dev -The default markdown parse mode now conforms to the commonmark specification ----------------------------------------------------------------------------- +Changes to the default parse mode +--------------------------------- + +The default markdown parse mode now conforms to the commonmark specification. The old markdown parser (which was used as the default ``client.parse_mode``) used to emulate Telegram Desktop's behaviour. Now ``__ @@ -224,6 +226,8 @@ Because now there's proper parsing, you also gain: * Inline links should no longer behave in a strange manner. * Pre-blocks can now have a language. Official clients don't syntax highlight code yet, though. +Furthermore, the parse mode is no longer client-dependant. It is now configured through ``Message``. + // TODO provide a way to get back the old behaviour? diff --git a/telethon/_client/messageparse.py b/telethon/_client/messageparse.py index b7e76e8d..f545a2f3 100644 --- a/telethon/_client/messageparse.py +++ b/telethon/_client/messageparse.py @@ -10,16 +10,6 @@ if typing.TYPE_CHECKING: from .telegramclient import TelegramClient -def get_parse_mode(self: 'TelegramClient'): - return self._parse_mode - -def set_parse_mode(self: 'TelegramClient', mode: str): - self._parse_mode = utils.sanitize_parse_mode(mode) - -# endregion - -# region Private methods - async def _replace_with_mention(self: 'TelegramClient', entities, i, user): """ Helper method to replace ``entities[i]`` to mention ``user``, diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 034b0a30..e9ea9ca9 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -1817,50 +1817,6 @@ class TelegramClient: # endregion Downloads - # region Message parse - - @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. - - When setting a different value it should be one of: - - * 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 ``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])``. - - The ``unparse`` method should be the inverse of ``parse`` such - that ``assert text == unparse(*parse(text))``. - - See :tl:`MessageEntity` for allowed message entities. - - Example - .. code-block:: python - - # Disabling default formatting - client.parse_mode = None - - # Enabling HTML as the default format - client.parse_mode = 'html' - """ - return messageparse.get_parse_mode(**locals()) - - @parse_mode.setter - def parse_mode(self: 'TelegramClient', mode: str): - return messageparse.set_parse_mode(**locals()) - - # endregion Message parse - # region Messages @forward_call(messages.get_messages) From 010ee0813a73b8c0e7748b9e91b0b3d9f3d76748 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 16 Oct 2021 14:01:56 +0200 Subject: [PATCH 122/256] Rename send_read_acknowledge --- readthedocs/misc/v2-migration-guide.rst | 2 ++ telethon/_client/messages.py | 2 +- telethon/_client/telegramclient.py | 10 +++++----- telethon/_events/album.py | 6 +++--- telethon/types/_custom/message.py | 6 +++--- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 0b6a7c01..774e8634 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -716,3 +716,5 @@ size selector for download_profile_photo and download_media is now different still thumb because otherwise documents are weird. keep support for explicit size instance? + +renamed send_read_acknowledge. add send_read_acknowledge as alias for mark_read? diff --git a/telethon/_client/messages.py b/telethon/_client/messages.py index 4a5ae371..f55fed8d 100644 --- a/telethon/_client/messages.py +++ b/telethon/_client/messages.py @@ -667,7 +667,7 @@ async def delete_messages( return sum(r.pts_count for r in res) -async def send_read_acknowledge( +async def mark_read( self: 'TelegramClient', entity: 'hints.EntityLike', message: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]' = None, diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index e9ea9ca9..f6ad961b 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -2490,8 +2490,8 @@ class TelegramClient: await client.delete_messages(chat, messages) """ - @forward_call(messages.send_read_acknowledge) - async def send_read_acknowledge( + @forward_call(messages.mark_read) + async def mark_read( self: 'TelegramClient', entity: 'hints.EntityLike', message: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]' = None, @@ -2535,11 +2535,11 @@ class TelegramClient: .. code-block:: python # using a Message object - await client.send_read_acknowledge(chat, message) + await client.mark_read(chat, message) # ...or using the int ID of a Message - await client.send_read_acknowledge(chat, message_id) + await client.mark_read(chat, message_id) # ...or passing a list of messages to mark as read - await client.send_read_acknowledge(chat, messages) + await client.mark_read(chat, messages) """ @forward_call(messages.pin_message) diff --git a/telethon/_events/album.py b/telethon/_events/album.py index cb9c39a8..19ea4234 100644 --- a/telethon/_events/album.py +++ b/telethon/_events/album.py @@ -310,12 +310,12 @@ class Album(EventBuilder): async def mark_read(self): """ Marks the entire album as read. Shorthand for - `client.send_read_acknowledge() - ` + `client.mark_read() + ` with both ``entity`` and ``message`` already set. """ if self._client: - await self._client.send_read_acknowledge( + await self._client.mark_read( await self.get_input_chat(), max_id=self.messages[-1].id) async def pin(self, *, notify=False): diff --git a/telethon/types/_custom/message.py b/telethon/types/_custom/message.py index bd0b61ee..278bc0e7 100644 --- a/telethon/types/_custom/message.py +++ b/telethon/types/_custom/message.py @@ -1237,12 +1237,12 @@ class Message(ChatGetter, SenderGetter): async def mark_read(self): """ Marks the message as read. Shorthand for - `client.send_read_acknowledge() - ` + `client.mark_read() + ` with both ``entity`` and ``message`` already set. """ if self._client: - await self._client.send_read_acknowledge( + await self._client.mark_read( await self.get_input_chat(), max_id=self.id) async def pin(self, *, notify=False, pm_oneside=False): From 232e76e73a34db706f08e98c0c1ce3abdf2080d2 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 9 Jan 2022 12:20:58 +0100 Subject: [PATCH 123/256] Stop setting the sender to be the channel when missing --- readthedocs/misc/v2-migration-guide.rst | 4 ++++ telethon/types/_custom/message.py | 7 ------- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 774e8634..96ef3b6e 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -494,6 +494,10 @@ empty media. The ``telethon.tl.patched`` hack has been removed. +The message sender no longer is the channel when no sender is provided by Telegram. Telethon used +to patch this value for channels to be the same as the chat, but now it will be faithful to +Telegram's value. + In order to avoid breaking more code than strictly necessary, ``.raw_text`` will remain a synonym of ``.message``, and ``.text`` will still be the text formatted through the ``client.parse_mode``. However, you're encouraged to change uses of ``.raw_text`` with ``.message``, and ``.text`` with diff --git a/telethon/types/_custom/message.py b/telethon/types/_custom/message.py index 278bc0e7..54dde6b0 100644 --- a/telethon/types/_custom/message.py +++ b/telethon/types/_custom/message.py @@ -410,13 +410,6 @@ class Message(ChatGetter, SenderGetter): if isinstance(message, _tl.Message): if message.from_id is not None: sender_id = utils.get_peer_id(message.from_id) - if sender_id is None and message.peer_id and not isinstance(message, _tl.MessageEmpty): - # If the message comes from a Channel, let the sender be it - # ...or... - # incoming messages in private conversations no longer have from_id - # (layer 119+), but the sender can only be the chat we're in. - if message.post or (not message.out and isinstance(message.peer_id, _tl.PeerUser)): - sender_id = utils.get_peer_id(message.peer_id) # Note that these calls would reset the client ChatGetter.__init__(self, message.peer_id, broadcast=message.post) From 721c803af99a17140d77bba37ce6720b81374828 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 9 Jan 2022 12:23:06 +0100 Subject: [PATCH 124/256] Stop opening webbrowser on clicking URL buttons --- readthedocs/misc/v2-migration-guide.rst | 6 ++++++ telethon/types/_custom/messagebutton.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 96ef3b6e..42285c1d 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -701,6 +701,12 @@ CdnDecrypter has been removed It was not really working and was more intended to be an implementation detail than anything else. +URL buttons no longer open the web-browser +------------------------------------------ + +Now the URL is returned. You can still use ``webbrowser.open`` to get the old behaviour. + + --- you can no longer pass an attributes list because the constructor is now nice. diff --git a/telethon/types/_custom/messagebutton.py b/telethon/types/_custom/messagebutton.py index c821c410..2d588727 100644 --- a/telethon/types/_custom/messagebutton.py +++ b/telethon/types/_custom/messagebutton.py @@ -112,7 +112,7 @@ class MessageButton: bot=self._bot, peer=self._chat, start_param=self.button.query )) elif isinstance(self.button, _tl.KeyboardButtonUrl): - return webbrowser.open(self.button.url) + return self.button.url elif isinstance(self.button, _tl.KeyboardButtonGame): req = _tl.fn.messages.GetBotCallbackAnswer( peer=self._chat, msg_id=self._msg_id, game=True From 7ea30961aeca35559b7d7244814f9e2d8a99dcb1 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 9 Jan 2022 13:00:45 +0100 Subject: [PATCH 125/256] Bump minimum required Python version to 3.7 --- .github/workflows/python.yml | 2 +- pyproject.toml | 2 +- readthedocs/basic/signing-in.rst | 19 +++++-------------- readthedocs/developing/testing.rst | 2 +- readthedocs/misc/v2-migration-guide.rst | 4 +++- setup.py | 6 +++--- telethon/_misc/hints.py | 18 +++++------------- 7 files changed, 19 insertions(+), 34 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index d3c34a20..f3fd3106 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.5", "3.6", "3.7", "3.8"] + python-version: ["3.7", "3.8", "3.9", "3.10"] steps: - uses: actions/checkout@v1 - name: Set up Python ${{ matrix.python-version }} diff --git a/pyproject.toml b/pyproject.toml index daae10fa..ca877203 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta" [tool.tox] legacy_tox_ini = """ [tox] -envlist = py35,py36,py37,py38 +envlist = py37,py38 # run with tox -e py [testenv] diff --git a/readthedocs/basic/signing-in.rst b/readthedocs/basic/signing-in.rst index 7f584a95..c7b60507 100644 --- a/readthedocs/basic/signing-in.rst +++ b/readthedocs/basic/signing-in.rst @@ -122,11 +122,9 @@ with `@BotFather `_. Signing In behind a Proxy ========================= -If you need to use a proxy to access Telegram, -you will need to either: +If you need to use a proxy to access Telegram, you will need to: -* For Python >= 3.6 : `install python-socks[asyncio]`__ -* For Python <= 3.5 : `install PySocks`__ +`install python-socks[asyncio]`__ and then change @@ -147,16 +145,9 @@ consisting of parameters described `in PySocks usage`__. The allowed values for the argument ``proxy_type`` are: -* For Python <= 3.5: - * ``socks.SOCKS5`` or ``'socks5'`` - * ``socks.SOCKS4`` or ``'socks4'`` - * ``socks.HTTP`` or ``'http'`` - -* For Python >= 3.6: - * All of the above - * ``python_socks.ProxyType.SOCKS5`` - * ``python_socks.ProxyType.SOCKS4`` - * ``python_socks.ProxyType.HTTP`` +* ``python_socks.ProxyType.SOCKS5`` +* ``python_socks.ProxyType.SOCKS4`` +* ``python_socks.ProxyType.HTTP`` Example: diff --git a/readthedocs/developing/testing.rst b/readthedocs/developing/testing.rst index badb7dc6..dfabe73f 100644 --- a/readthedocs/developing/testing.rst +++ b/readthedocs/developing/testing.rst @@ -71,7 +71,7 @@ version incompatabilities. Tox environments are declared in the ``tox.ini`` file. The default environments, declared at the top, can be simply run with ``tox``. The option -``tox -e py36,flake`` can be used to request specific environments to be run. +``tox -e py37,flake`` can be used to request specific environments to be run. Brief Introduction to Pytest-cov ================================ diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 42285c1d..d9e3c270 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -16,7 +16,9 @@ good chance you were not relying on this to begin with". Python 3.5 is no longer supported --------------------------------- -The library will no longer attempt to support Python 3.5. The minimum version is now Python 3.6. +The library will no longer attempt to support Python 3.5. The minimum version is now Python 3.7. + +This also means workarounds for 3.6 and below have been dropped. User, chat and channel identifiers are now 64-bit numbers diff --git a/setup.py b/setup.py index 05f0f7b6..caf4a54a 100755 --- a/setup.py +++ b/setup.py @@ -208,7 +208,7 @@ def main(argv): # See https://stackoverflow.com/a/40300957/4759433 # -> https://www.python.org/dev/peps/pep-0345/#requires-python # -> http://setuptools.readthedocs.io/en/latest/setuptools.html - python_requires='>=3.5', + python_requires='>=3.7', # See https://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[ @@ -223,10 +223,10 @@ def main(argv): 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', ], keywords='telegram api chat client library messaging mtproto', packages=find_packages(exclude=[ diff --git a/telethon/_misc/hints.py b/telethon/_misc/hints.py index 47f26a8f..52951cec 100644 --- a/telethon/_misc/hints.py +++ b/telethon/_misc/hints.py @@ -48,19 +48,11 @@ FileLike = typing.Union[ _tl.TypeInputFileLocation ] -# Can't use `typing.Type` in Python 3.5.2 -# See https://github.com/python/typing/issues/266 -try: - OutFileLike = typing.Union[ - str, - typing.Type[bytes], - typing.BinaryIO - ] -except TypeError: - OutFileLike = typing.Union[ - str, - typing.BinaryIO - ] +OutFileLike = typing.Union[ + str, + typing.Type[bytes], + typing.BinaryIO +] MessageLike = typing.Union[str, _tl.Message] MessageIDLike = typing.Union[int, _tl.Message, _tl.TypeInputMessage] From be6508dc5d0d28d75a550cc910805af3e92d74a0 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 9 Jan 2022 13:01:16 +0100 Subject: [PATCH 126/256] Use frozen dataclasses for session types Now that 3.7 is the minimum version, we can use dataclasses. --- telethon/_sessions/abstract.py | 12 +-- telethon/_sessions/types.py | 135 ++++++++++++--------------------- 2 files changed, 53 insertions(+), 94 deletions(-) diff --git a/telethon/_sessions/abstract.py b/telethon/_sessions/abstract.py index 2b28ae76..cdb747a4 100644 --- a/telethon/_sessions/abstract.py +++ b/telethon/_sessions/abstract.py @@ -1,4 +1,4 @@ -from .types import DataCenter, ChannelState, SessionState, Entity +from .types import DataCenter, ChannelState, SessionState, EntityType, Entity from abc import ABC, abstractmethod from typing import List, Optional @@ -59,7 +59,7 @@ class Session(ABC): raise NotImplementedError @abstractmethod - async def get_entity(self, ty: Optional[int], id: int) -> Optional[Entity]: + async def get_entity(self, ty: Optional[EntityType], id: int) -> Optional[Entity]: """ Get the `Entity` with matching ``ty`` and ``id``. @@ -67,14 +67,14 @@ class Session(ABC): ``ty`` and ``id``, if the ``ty`` is in a given group, a matching ``access_hash`` with that ``id`` from within any ``ty`` in that group should be returned. - * ``'U'`` and ``'B'`` (user and bot). - * ``'G'`` (small group chat). - * ``'C'``, ``'M'`` and ``'E'`` (broadcast channel, megagroup channel, and gigagroup channel). + * `EntityType.USER` and `EntityType.BOT`. + * `EntityType.GROUP`. + * `EntityType.CHANNEL`, `EntityType.MEGAGROUP` and `EntityType.GIGAGROUP`. For example, if a ``ty`` representing a bot is stored but the asking ``ty`` is a user, the corresponding ``access_hash`` should still be returned. - You may use `types.canonical_entity_type` to find out the canonical type. + You may use ``EntityType.canonical`` to find out the canonical type. A ``ty`` with the value of ``None`` should be treated as "any entity with matching ID". """ diff --git a/telethon/_sessions/types.py b/telethon/_sessions/types.py index 51d4ecb5..a9738709 100644 --- a/telethon/_sessions/types.py +++ b/telethon/_sessions/types.py @@ -1,6 +1,9 @@ from typing import Optional, Tuple +from dataclasses import dataclass +from enum import IntEnum +@dataclass(frozen=True) class DataCenter: """ Stores the information needed to connect to a datacenter. @@ -12,21 +15,14 @@ class DataCenter: """ __slots__ = ('id', 'ipv4', 'ipv6', 'port', 'auth') - def __init__( - self, - id: int, - ipv4: int, - ipv6: Optional[int], - port: int, - auth: bytes - ): - self.id = id - self.ipv4 = ipv4 - self.ipv6 = ipv6 - self.port = port - self.auth = auth + id: int + ipv4: int + ipv6: Optional[int] + port: int + auth: bytes +@dataclass(frozen=True) class SessionState: """ Stores the information needed to fetch updates and about the current user. @@ -45,27 +41,17 @@ class SessionState: """ __slots__ = ('user_id', 'dc_id', 'bot', 'pts', 'qts', 'date', 'seq', 'takeout_id') - def __init__( - self, - user_id: int, - dc_id: int, - bot: bool, - pts: int, - qts: int, - date: int, - seq: int, - takeout_id: Optional[int], - ): - self.user_id = user_id - self.dc_id = dc_id - self.bot = bot - self.pts = pts - self.qts = qts - self.date = date - self.seq = seq - self.takeout_id = takeout_id + user_id: int + dc_id: int + bot: bool + pts: int + qts: int + date: int + seq: int + takeout_id: Optional[int] +@dataclass(frozen=True) class ChannelState: """ Stores the information needed to fetch updates from a channel. @@ -75,24 +61,13 @@ class ChannelState: """ __slots__ = ('channel_id', 'pts') - def __init__( - self, - channel_id: int, - pts: int - ): - self.channel_id = channel_id - self.pts = pts + channel_id: int + pts: int -class Entity: +class EntityType(IntEnum): """ - Stores the information needed to use a certain user, chat or channel with the API. - - * ty: 8-bit number indicating the type of the entity. - * id: 64-bit number uniquely identifying the entity among those of the same type. - * access_hash: 64-bit number needed to use this entity with the API. - - You can rely on the ``ty`` value to be equal to the ASCII character one of: + You can rely on the type value to be equal to the ASCII character one of: * 'U' (85): this entity belongs to a :tl:`User` who is not a ``bot``. * 'B' (66): this entity belongs to a :tl:`User` who is a ``bot``. @@ -101,8 +76,6 @@ class Entity: * 'M' (77): this entity belongs to a megagroup :tl:`Channel`. * 'E' (69): this entity belongs to an "enormous" "gigagroup" :tl:`Channel`. """ - __slots__ = ('ty', 'id', 'access_hash') - USER = ord('U') BOT = ord('B') GROUP = ord('G') @@ -110,48 +83,34 @@ class Entity: MEGAGROUP = ord('M') GIGAGROUP = ord('E') - def __init__( - self, - ty: int, - id: int, - access_hash: int - ): - self.ty = ty - self.id = id - self.access_hash = access_hash + def canonical(self): + """ + Return the canonical version of this type. + """ + return _canon_entity_types[self] -def canonical_entity_type(ty: int, *, _mapping={ - Entity.USER: Entity.USER, - Entity.BOT: Entity.USER, - Entity.GROUP: Entity.GROUP, - Entity.CHANNEL: Entity.CHANNEL, - Entity.MEGAGROUP: Entity.CHANNEL, - Entity.GIGAGROUP: Entity.CHANNEL, -}) -> int: - """ - Return the canonical version of an entity type. - """ - try: - return _mapping[ty] - except KeyError: - ty = chr(ty) if isinstance(ty, int) else ty - raise ValueError(f'entity type {ty!r} is not valid') +_canon_entity_types = { + EntityType.USER: EntityType.USER, + EntityType.BOT: EntityType.USER, + EntityType.GROUP: EntityType.GROUP, + EntityType.CHANNEL: EntityType.CHANNEL, + EntityType.MEGAGROUP: EntityType.CHANNEL, + EntityType.GIGAGROUP: EntityType.CHANNEL, +} -def get_entity_type_group(ty: int, *, _mapping={ - Entity.USER: (Entity.USER, Entity.BOT), - Entity.BOT: (Entity.USER, Entity.BOT), - Entity.GROUP: (Entity.GROUP,), - Entity.CHANNEL: (Entity.CHANNEL, Entity.MEGAGROUP, Entity.GIGAGROUP), - Entity.MEGAGROUP: (Entity.CHANNEL, Entity.MEGAGROUP, Entity.GIGAGROUP), - Entity.GIGAGROUP: (Entity.CHANNEL, Entity.MEGAGROUP, Entity.GIGAGROUP), -}) -> Tuple[int]: +@dataclass(frozen=True) +class Entity: """ - Return the group where an entity type belongs to. + Stores the information needed to use a certain user, chat or channel with the API. + + * ty: 8-bit number indicating the type of the entity. + * id: 64-bit number uniquely identifying the entity among those of the same type. + * access_hash: 64-bit number needed to use this entity with the API. """ - try: - return _mapping[ty] - except KeyError: - ty = chr(ty) if isinstance(ty, int) else ty - raise ValueError(f'entity type {ty!r} is not valid') + __slots__ = ('ty', 'id', 'access_hash') + + ty: EntityType + id: int + access_hash: int From 691160bd92ed0aa39ea551f6a9bb310ab9881cfa Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 9 Jan 2022 13:03:02 +0100 Subject: [PATCH 127/256] Remove 3.7 workarounds --- telethon/_network/connection/connection.py | 17 +++++++------- telethon/errors/__init__.py | 26 +++------------------- 2 files changed, 11 insertions(+), 32 deletions(-) diff --git a/telethon/_network/connection/connection.py b/telethon/_network/connection/connection.py index 4570180d..abf398ee 100644 --- a/telethon/_network/connection/connection.py +++ b/telethon/_network/connection/connection.py @@ -263,15 +263,14 @@ class Connection(abc.ABC): if self._writer: self._writer.close() - if sys.version_info >= (3, 7): - try: - await self._writer.wait_closed() - except Exception as e: - # Disconnecting should never raise. Seen: - # * OSError: No route to host and - # * OSError: [Errno 32] Broken pipe - # * ConnectionResetError - self._log.info('%s during disconnect: %s', type(e), e) + try: + await self._writer.wait_closed() + except Exception as e: + # Disconnecting should never raise. Seen: + # * OSError: No route to host and + # * OSError: [Errno 32] Broken pipe + # * ConnectionResetError + self._log.info('%s during disconnect: %s', type(e), e) def send(self, data): """ diff --git a/telethon/errors/__init__.py b/telethon/errors/__init__.py index 152e7823..0d4ea0cc 100644 --- a/telethon/errors/__init__.py +++ b/telethon/errors/__init__.py @@ -1,5 +1,3 @@ -import sys - from ._custom import ( ReadCancelledError, TypeNotFoundError, @@ -25,24 +23,6 @@ from ._rpcbase import ( _mk_error_type ) -if sys.version_info < (3, 7): - # https://stackoverflow.com/a/7668273/ - class _TelethonErrors: - def __init__(self, _mk_error_type, everything): - self._mk_error_type = _mk_error_type - self.__dict__.update({ - k: v - for k, v in everything.items() - if isinstance(v, type) and issubclass(v, Exception) - }) - - def __getattr__(self, name): - return self._mk_error_type(name=name) - - sys.modules[__name__] = _TelethonErrors(_mk_error_type, globals()) -else: - # https://www.python.org/dev/peps/pep-0562/ - def __getattr__(name): - return _mk_error_type(name=name) - -del sys +# https://www.python.org/dev/peps/pep-0562/ +def __getattr__(name): + return _mk_error_type(name=name) From 2db0725b983a7a6b17de47cb0a89d8cb84904e2a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 9 Jan 2022 14:41:04 +0100 Subject: [PATCH 128/256] Fix generating error names in TL ref --- telethon_generator/generators/docs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon_generator/generators/docs.py b/telethon_generator/generators/docs.py index 8b46e4d1..50a856b0 100755 --- a/telethon_generator/generators/docs.py +++ b/telethon_generator/generators/docs.py @@ -370,7 +370,7 @@ def _write_html_pages(tlobjects, methods, layer, input_res): )) docs.begin_table(column_count=2) for error in errors: - docs.add_row('{}'.format(error.name)) + docs.add_row('{}'.format(error.canonical_name)) docs.add_row('{}.'.format(error.description)) docs.end_table() docs.write_text('You can import these from ' From be0da9b1835793127f7983538ce1be178faabe17 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 9 Jan 2022 14:41:10 +0100 Subject: [PATCH 129/256] Update takeout to use less hacks --- readthedocs/misc/v2-migration-guide.rst | 18 +++ telethon/__init__.py | 1 + telethon/_client/account.py | 154 +++++++----------------- telethon/_client/telegramclient.py | 77 ++++++++---- telethon/_client/users.py | 4 + 5 files changed, 118 insertions(+), 136 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index d9e3c270..97b0bcb2 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -697,6 +697,24 @@ If you were relying on any of the individual mixins that made up the client, suc There is a single ``TelegramClient`` class now, containing everything you need. +The takeout context-manager has changed +--------------------------------------- + +It no longer has a finalize. All the requests made by the client in the same task will be wrapped, +not only those made through the proxy client returned by the context-manager. + +This cleans up the (rather hacky) implementation, making use of Python's ``contextvar``. If you +still need the takeout session to persist, you should manually use the ``begin_takeout`` and +``end_takeout`` method. + +If you want to ignore the currently-active takeout session in a task, toggle the following context +variable: + +.. code-block:: python + + telethon.ignore_takeout.set(True) + + CdnDecrypter has been removed ----------------------------- diff --git a/telethon/__init__.py b/telethon/__init__.py index 86e0580f..5d337667 100644 --- a/telethon/__init__.py +++ b/telethon/__init__.py @@ -5,6 +5,7 @@ from ._misc import utils as _ # depends on helpers and _tl from ._misc import hints as _ # depends on types/custom from ._client.telegramclient import TelegramClient +from ._client.account import ignore_takeout from . import version, events, errors, enums __version__ = version.__version__ diff --git a/telethon/_client/account.py b/telethon/_client/account.py index eedad595..8c25b232 100644 --- a/telethon/_client/account.py +++ b/telethon/_client/account.py @@ -1,6 +1,7 @@ import functools import inspect import typing +from contextvars import ContextVar from .users import _NOT_A_REQUEST from .._misc import helpers, utils @@ -10,112 +11,43 @@ if typing.TYPE_CHECKING: from .telegramclient import TelegramClient +ignore_takeout = ContextVar('ignore_takeout', default=False) + + # TODO Make use of :tl:`InvokeWithMessagesRange` somehow # For that, we need to use :tl:`GetSplitRanges` first. -class _TakeoutClient: - """ - Proxy object over the client. - """ - __PROXY_INTERFACE = ('__enter__', '__exit__', '__aenter__', '__aexit__') - - def __init__(self, finalize, client, request): - # We use the name mangling for attributes to make them inaccessible - # from within the shadowed client object and to distinguish them from - # its own attributes where needed. - self.__finalize = finalize - self.__client = client - self.__request = request - self.__success = None - - @property - def success(self): - return self.__success - - @success.setter - def success(self, value): - self.__success = value +class _Takeout: + def __init__(self, client, kwargs): + self._client = client + self._kwargs = kwargs async def __aenter__(self): - # Enter/Exit behaviour is "overrode", we don't want to call start. - client = self.__client - if client.session.takeout_id is None: - client.session.takeout_id = (await client(self.__request)).id - elif self.__request is not None: - raise ValueError("Can't send a takeout request while another " - "takeout for the current session still not been finished yet.") - return self + await self._client.begin_takeout(**kwargs) + return self._client async def __aexit__(self, exc_type, exc_value, traceback): - if self.__success is None and self.__finalize: - self.__success = exc_type is None - - if self.__success is not None: - result = await self(_tl.fn.account.FinishTakeoutSession( - self.__success)) - if not result: - raise ValueError("Failed to finish the takeout.") - self.session.takeout_id = None - - async def __call__(self, request, ordered=False): - takeout_id = self.__client.session.takeout_id - if takeout_id is None: - raise ValueError('Takeout mode has not been initialized ' - '(are you calling outside of "with"?)') - - single = not utils.is_list_like(request) - requests = ((request,) if single else request) - wrapped = [] - for r in requests: - if not isinstance(r, _tl.TLRequest): - raise _NOT_A_REQUEST() - await r.resolve(self, utils) - wrapped.append(_tl.fn.InvokeWithTakeout(takeout_id, r)) - - return await self.__client( - wrapped[0] if single else wrapped, ordered=ordered) - - def __getattribute__(self, name): - # We access class via type() because __class__ will recurse infinitely. - # Also note that since we've name-mangled our own class attributes, - # they'll be passed to __getattribute__() as already decorated. For - # example, 'self.__client' will be passed as '_TakeoutClient__client'. - # https://docs.python.org/3/tutorial/classes.html#private-variables - if name.startswith('__') and name not in type(self).__PROXY_INTERFACE: - raise AttributeError # force call of __getattr__ - - # Try to access attribute in the proxy object and check for the same - # attribute in the shadowed object (through our __getattr__) if failed. - return super().__getattribute__(name) - - def __getattr__(self, name): - value = getattr(self.__client, name) - if inspect.ismethod(value): - # Emulate bound methods behavior by partially applying our proxy - # class as the self parameter instead of the client. - return functools.partial( - getattr(self.__client.__class__, name), self) - - return value - - def __setattr__(self, name, value): - if name.startswith('_{}__'.format(type(self).__name__.lstrip('_'))): - # This is our own name-mangled attribute, keep calm. - return super().__setattr__(name, value) - return setattr(self.__client, name, value) + await self._client.end_takeout(success=exc_type is None) -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( +def takeout(self: 'TelegramClient', **kwargs): + return _Takeout(self, kwargs) + + +async def begin_takeout( + self: 'TelegramClient', + *, + contacts: bool = None, + users: bool = None, + chats: bool = None, + megagroups: bool = None, + channels: bool = None, + files: bool = None, + max_file_size: bool = None, +) -> 'TelegramClient': + if takeout_active(): + raise ValueError('a previous takeout session was already active') + + self._session_state.takeout_id = (await client( contacts=contacts, message_users=users, message_chats=chats, @@ -123,21 +55,19 @@ def takeout( message_channels=channels, files=files, file_max_size=max_file_size - ) - arg_specified = (arg is not None for arg in request_kwargs.values()) + )).id - if self.session.takeout_id is None or any(arg_specified): - request = _tl.fn.account.InitTakeoutSession( - **request_kwargs) - else: - request = None - return _TakeoutClient(finalize, self, request) +def takeout_active(self: 'TelegramClient') -> bool: + return self._session_state.takeout_id is not None + 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 + if not takeout_active(): + raise ValueError('no previous takeout session was active') + + result = await self(_tl.fn.account.FinishTakeoutSession(success)) + if not result: + raise ValueError("could not end the active takeout session") + + self._session_state.takeout_id = None diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index f6ad961b..5dfc0c92 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -174,7 +174,6 @@ class TelegramClient: @forward_call(account.takeout) def takeout( self: 'TelegramClient', - finalize: bool = True, *, contacts: bool = None, users: bool = None, @@ -184,14 +183,39 @@ class TelegramClient: files: bool = None, max_file_size: bool = None) -> 'TelegramClient': """ - Returns a :ref:`telethon-client` which calls methods behind a takeout session. + Returns a context-manager which calls `TelegramClient.begin_takeout` + on enter and `TelegramClient.end_takeout` on exit. The same errors + and conditions apply. - It does so by creating a proxy object over the current client through - which making requests will use :tl:`InvokeWithTakeout` to wrap - them. In other words, returns the current client modified so that - requests are done as a takeout: + This is useful for the common case of not wanting the takeout to + persist (although it still might if a disconnection occurs before it + can be ended). - Some of the calls made through the takeout session will have lower + Example + .. code-block:: python + + async with client.takeout(): + async for message in client.iter_messages(chat, wait_time=0): + ... # Do something with the message + """ + + @forward_call(account.begin_takeout) + def begin_takeout( + self: 'TelegramClient', + *, + contacts: bool = None, + users: bool = None, + chats: bool = None, + megagroups: bool = None, + channels: bool = None, + files: bool = None, + max_file_size: bool = None) -> 'TelegramClient': + """ + Begin a takeout session. All subsequent requests made by the client + will be behind a takeout session. The takeout session will persist + in the session file, until `TelegramClient.end_takeout` is used. + + When the takeout session is enabled, some requests 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 @@ -206,20 +230,16 @@ class TelegramClient: 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 in the session. + If you want to ignore the currently-active takeout session in a task, + toggle the following context variable: + + .. code-block:: python + + telethon.ignore_takeout.set(True) + + An error occurs if ``TelegramClient.takeout_active`` was already ``True``. 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. @@ -253,17 +273,26 @@ class TelegramClient: 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) + await client.begin_takeout() - async for message in takeout.iter_messages(chat, wait_time=0): - ... # Do something with the message + await client.get_messages('me') # wrapped through takeout (less limits) + + async for message in client.iter_messages(chat, wait_time=0): + ... # Do something with the message + + await client.end_takeout(success=True) except errors.TakeoutInitDelayError as e: print('Must wait', e.seconds, 'before takeout') + + except Exception: + await client.end_takeout(success=False) """ + @property + def takeout_active(self: 'TelegramClient') -> bool: + return account.takeout_active(self) + @forward_call(account.end_takeout) async def end_takeout(self: 'TelegramClient', success: bool) -> bool: """ diff --git a/telethon/_client/users.py b/telethon/_client/users.py index 02dccf95..69f2c564 100644 --- a/telethon/_client/users.py +++ b/telethon/_client/users.py @@ -9,6 +9,7 @@ from ..errors._rpcbase import RpcError, ServerError, FloodError, InvalidDcError, from .._misc import helpers, utils, hints from .._sessions.types import Entity from .. import errors, _tl +from .account import ignore_takeout _NOT_A_REQUEST = lambda: TypeError('You can only invoke requests, not types!') @@ -52,6 +53,9 @@ async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sl else: raise errors.FLOOD_WAIT(420, f'FLOOD_WAIT_{diff}', request=r) + if self._session_state.takeout_id and not ignore_takeout.get(): + r = _tl.fn.InvokeWithTakeout(self._session_state.takeout_id, r) + if self._no_updates: r = _tl.fn.InvokeWithoutUpdates(r) From 7524b652c835bb66c4d81e117ed0ce932ea4f4cf Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 15 Jan 2022 11:22:33 +0100 Subject: [PATCH 130/256] Unify setting session state --- telethon/_client/account.py | 7 ++++--- telethon/_client/auth.py | 29 ++++++++++++++++++-------- telethon/_client/telegrambaseclient.py | 10 ++------- telethon/_client/telegramclient.py | 6 +++++- 4 files changed, 31 insertions(+), 21 deletions(-) diff --git a/telethon/_client/account.py b/telethon/_client/account.py index 8c25b232..73673b8e 100644 --- a/telethon/_client/account.py +++ b/telethon/_client/account.py @@ -1,6 +1,7 @@ import functools import inspect import typing +import dataclasses from contextvars import ContextVar from .users import _NOT_A_REQUEST @@ -47,7 +48,7 @@ async def begin_takeout( if takeout_active(): raise ValueError('a previous takeout session was already active') - self._session_state.takeout_id = (await client( + await self._replace_session_state(takeout_id=(await client( contacts=contacts, message_users=users, message_chats=chats, @@ -55,7 +56,7 @@ async def begin_takeout( message_channels=channels, files=files, file_max_size=max_file_size - )).id + )).id) def takeout_active(self: 'TelegramClient') -> bool: @@ -70,4 +71,4 @@ async def end_takeout(self: 'TelegramClient', success: bool) -> bool: if not result: raise ValueError("could not end the active takeout session") - self._session_state.takeout_id = None + await self._replace_session_state(takeout_id=None) diff --git a/telethon/_client/auth.py b/telethon/_client/auth.py index 031b3a11..50df992b 100644 --- a/telethon/_client/auth.py +++ b/telethon/_client/auth.py @@ -5,6 +5,7 @@ import sys import typing import warnings import functools +import dataclasses from .._misc import utils, helpers, password as pwd_mod from .. import errors, _tl @@ -308,6 +309,7 @@ async def sign_up( return await _update_session_state(self, result.user) + async def _update_session_state(self, user, save=True): """ Callback called whenever the login or sign up process completes. @@ -315,20 +317,29 @@ async def _update_session_state(self, user, save=True): """ self._authorized = True - self._session_state.user_id = user.id - self._session_state.bot = user.bot - state = await self(_tl.fn.updates.GetState()) - self._session_state.pts = state.pts - self._session_state.qts = state.qts - self._session_state.date = int(state.date.timestamp()) - self._session_state.seq = state.seq + await _replace_session_state( + self, + save=save, + user_id=user.id, + bot=user.bot, + pts=state.pts, + qts=state.qts, + date=int(state.date.timestamp()), + seq=state.seq, + ) + + return user + + +async def _replace_session_state(self, *, save=True, **changes): + new = dataclasses.replace(self._session_state, **changes) + await self.session.set_state(new) + self._session_state = new - await self.session.set_state(self._session_state) if save: await self.session.save() - return user async def send_code_request( self: 'TelegramClient', diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 9316668b..85512cfb 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -445,10 +445,7 @@ async def _disconnect_coro(self: 'TelegramClient'): pts, date = self._state_cache[None] if pts and date: if self._session_state: - self._session_state.pts = pts - self._session_state.date = date - await self.session.set_state(self._session_state) - await self.session.save() + await self._replace_session_state(pts=pts, date=date) async def _disconnect(self: 'TelegramClient'): """ @@ -467,10 +464,7 @@ async def _switch_dc(self: 'TelegramClient', new_dc): """ self._log[__name__].info('Reconnecting to new data center %s', new_dc) - self._session_state.dc_id = new_dc - await self.session.set_state(self._session_state) - await self.session.save() - + await self._replace_session_state(dc_id=new_dc) await _disconnect(self) return await self.connect() diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 5dfc0c92..9755d437 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -3547,7 +3547,11 @@ class TelegramClient: pass @forward_call(auth._update_session_state) - async def _update_session_state(self, user, save=True): + async def _update_session_state(self, user, *, save=True): + pass + + @forward_call(auth._replace_session_state) + async def _replace_session_state(self, *, save=True, **changes): pass # endregion Private From 02703e37533cbd4d3dfecd26ab04f0aaf6422209 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 15 Jan 2022 13:18:53 +0100 Subject: [PATCH 131/256] Fix circular import regarding ignore_takeout --- telethon/__init__.py | 2 +- telethon/_client/account.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/telethon/__init__.py b/telethon/__init__.py index 5d337667..653356d1 100644 --- a/telethon/__init__.py +++ b/telethon/__init__.py @@ -3,9 +3,9 @@ from ._misc import helpers as _ # no dependencies from . import _tl # no dependencies from ._misc import utils as _ # depends on helpers and _tl from ._misc import hints as _ # depends on types/custom +from ._client.account import ignore_takeout from ._client.telegramclient import TelegramClient -from ._client.account import ignore_takeout from . import version, events, errors, enums __version__ = version.__version__ diff --git a/telethon/_client/account.py b/telethon/_client/account.py index 73673b8e..cdf79850 100644 --- a/telethon/_client/account.py +++ b/telethon/_client/account.py @@ -4,7 +4,6 @@ import typing import dataclasses from contextvars import ContextVar -from .users import _NOT_A_REQUEST from .._misc import helpers, utils from .. import _tl From f5f0c8455357a833db73fd7746cf122f3c1eb6dc Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 15 Jan 2022 13:33:50 +0100 Subject: [PATCH 132/256] Completely overhaul connections and transports Reduce abstraction leaks. Now the transport can hold any state, rather than just the tag. It's also responsible to initialize on the first connection, and they can be cleanly reset. asyncio connections are no longer used, in favour of raw sockets, which should avoid some annoyances. For the time being, more obscure transport modes have been removed, as well as proxy support, until further cleaning is done. --- telethon/_client/telegrambaseclient.py | 54 +-- telethon/_client/telegramclient.py | 1 - telethon/_misc/enums.py | 2 - telethon/_network/__init__.py | 9 +- telethon/_network/connection.py | 61 +++ telethon/_network/connection/__init__.py | 12 - telethon/_network/connection/connection.py | 426 ------------------ telethon/_network/connection/http.py | 39 -- telethon/_network/connection/tcpabridged.py | 33 -- telethon/_network/connection/tcpfull.py | 43 -- .../_network/connection/tcpintermediate.py | 46 -- telethon/_network/connection/tcpmtproxy.py | 152 ------- telethon/_network/connection/tcpobfuscated.py | 62 --- telethon/_network/transports/__init__.py | 4 + telethon/_network/transports/abridged.py | 43 ++ telethon/_network/transports/full.py | 41 ++ telethon/_network/transports/intermediate.py | 29 ++ telethon/_network/transports/transport.py | 17 + 18 files changed, 221 insertions(+), 853 deletions(-) create mode 100644 telethon/_network/connection.py delete mode 100644 telethon/_network/connection/__init__.py delete mode 100644 telethon/_network/connection/connection.py delete mode 100644 telethon/_network/connection/http.py delete mode 100644 telethon/_network/connection/tcpabridged.py delete mode 100644 telethon/_network/connection/tcpfull.py delete mode 100644 telethon/_network/connection/tcpintermediate.py delete mode 100644 telethon/_network/connection/tcpmtproxy.py delete mode 100644 telethon/_network/connection/tcpobfuscated.py create mode 100644 telethon/_network/transports/__init__.py create mode 100644 telethon/_network/transports/abridged.py create mode 100644 telethon/_network/transports/full.py create mode 100644 telethon/_network/transports/intermediate.py create mode 100644 telethon/_network/transports/transport.py diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 85512cfb..e4906fbf 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -11,7 +11,7 @@ import ipaddress from .. import version, __name__ as __base_name__, _tl from .._crypto import rsa from .._misc import markdown, entitycache, statecache, enums, helpers -from .._network import MTProtoSender, Connection, ConnectionTcpFull, connection as conns +from .._network import MTProtoSender, Connection, transports from .._sessions import Session, SQLiteSession, MemorySession from .._sessions.types import DataCenter, SessionState @@ -70,7 +70,7 @@ def init( api_id: int, api_hash: str, *, - connection: 'typing.Type[Connection]' = ConnectionTcpFull, + connection: 'typing.Type[Connection]' = (), use_ipv6: bool = False, proxy: typing.Union[tuple, dict] = None, local_addr: typing.Union[str, tuple] = None, @@ -194,15 +194,12 @@ def init( # For now the current default remains TCP Full; may change to be "smart" if proxies are specified connection = enums.ConnectionMode.FULL - self._connection = { - enums.ConnectionMode.FULL: conns.ConnectionTcpFull, - enums.ConnectionMode.INTERMEDIATE: conns.ConnectionTcpIntermediate, - enums.ConnectionMode.ABRIDGED: conns.ConnectionTcpAbridged, - enums.ConnectionMode.OBFUSCATED: conns.ConnectionTcpObfuscated, - enums.ConnectionMode.HTTP: conns.ConnectionHttp, + self._transport = { + enums.ConnectionMode.FULL: transports.Full(), + enums.ConnectionMode.INTERMEDIATE: transports.Intermediate(), + enums.ConnectionMode.ABRIDGED: transports.Abridged(), }[enums.parse_conn_mode(connection)] - init_proxy = None if not issubclass(self._connection, conns.TcpMTProxy) else \ - _tl.InputClientProxy(*self._connection.address_info(proxy)) + init_proxy = None # Used on connection. Capture the variables in a lambda since # exporting clients need to create this InvokeWithLayer. @@ -334,13 +331,12 @@ async def connect(self: 'TelegramClient') -> None: # Use known key, if any self._sender.auth_key.key = dc.auth - if not await self._sender.connect(self._connection( - str(ipaddress.ip_address((self._use_ipv6 and dc.ipv6) or dc.ipv4)), - dc.port, - dc.id, + if not await self._sender.connect(Connection( + ip=str(ipaddress.ip_address((self._use_ipv6 and dc.ipv6) or dc.ipv4)), + port=dc.port, + transport=self._transport.recreate_fresh(), loggers=self._log, - proxy=self._proxy, - local_addr=self._local_addr + local_addr=self._local_addr, )): # We don't want to init or modify anything if we were already connected return @@ -396,8 +392,7 @@ async def disconnect(self: 'TelegramClient'): return await _disconnect_coro(self) def set_proxy(self: 'TelegramClient', proxy: typing.Union[tuple, dict]): - init_proxy = None if not issubclass(self._connection, conns.TcpMTProxy) else \ - _tl.InputClientProxy(*self._connection.address_info(proxy)) + init_proxy = None self._init_request.proxy = init_proxy self._proxy = proxy @@ -481,13 +476,12 @@ async def _create_exported_sender(self: 'TelegramClient', dc_id): # If one were to do that, Telegram would reset the connection # with no further clues. sender = MTProtoSender(loggers=self._log) - await sender.connect(self._connection( - str(ipaddress.ip_address((self._use_ipv6 and dc.ipv6) or dc.ipv4)), - dc.port, - dc.id, + await self._sender.connect(Connection( + ip=str(ipaddress.ip_address((self._use_ipv6 and dc.ipv6) or dc.ipv4)), + port=dc.port, + transport=self._transport.recreate_fresh(), loggers=self._log, - proxy=self._proxy, - local_addr=self._local_addr + local_addr=self._local_addr, )) self._log[__name__].info('Exporting auth for new borrowed sender in %s', dc) auth = await self(_tl.fn.auth.ExportAuthorization(dc_id)) @@ -516,13 +510,13 @@ async def _borrow_exported_sender(self: 'TelegramClient', dc_id): elif state.need_connect(): dc = self._all_dcs[dc_id] - await sender.connect(self._connection( - str(ipaddress.ip_address((self._use_ipv6 and dc.ipv6) or dc.ipv4)), - dc.port, - dc.id, + + await self._sender.connect(Connection( + ip=str(ipaddress.ip_address((self._use_ipv6 and dc.ipv6) or dc.ipv4)), + port=dc.port, + transport=self._transport.recreate_fresh(), loggers=self._log, - proxy=self._proxy, - local_addr=self._local_addr + local_addr=self._local_addr, )) state.add_borrow() diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 9755d437..bb21a416 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -10,7 +10,6 @@ from . import ( ) from .. import version, _tl from ..types import _custom -from .._network import ConnectionTcpFull from .._events.common import EventBuilder, EventCommon from .._misc import enums diff --git a/telethon/_misc/enums.py b/telethon/_misc/enums.py index 2d5742aa..283030f3 100644 --- a/telethon/_misc/enums.py +++ b/telethon/_misc/enums.py @@ -15,8 +15,6 @@ class ConnectionMode(Enum): FULL = 'full' INTERMEDIATE = 'intermediate' ABRIDGED = 'abridged' - OBFUSCATED = 'obfuscated' - HTTP = 'http' class Participant(Enum): diff --git a/telethon/_network/__init__.py b/telethon/_network/__init__.py index 0b985d58..164acc4e 100644 --- a/telethon/_network/__init__.py +++ b/telethon/_network/__init__.py @@ -5,10 +5,5 @@ with Telegram's servers and the protocol used (TCP full, abridged, etc.). from .mtprotoplainsender import MTProtoPlainSender from .authenticator import do_authentication from .mtprotosender import MTProtoSender -from .connection import ( - Connection, - ConnectionTcpFull, ConnectionTcpIntermediate, ConnectionTcpAbridged, - ConnectionTcpObfuscated, ConnectionTcpMTProxyAbridged, - ConnectionTcpMTProxyIntermediate, - ConnectionTcpMTProxyRandomizedIntermediate, ConnectionHttp, TcpMTProxy -) +from .connection import Connection +from . import transports diff --git a/telethon/_network/connection.py b/telethon/_network/connection.py new file mode 100644 index 00000000..26674aa2 --- /dev/null +++ b/telethon/_network/connection.py @@ -0,0 +1,61 @@ +import asyncio +import socket + +from .transports.transport import Transport + + +CHUNK_SIZE = 32 * 1024 + + +# TODO ideally the mtproto impl would also be sans-io, but that's less pressing +class Connection: + def __init__(self, ip, port, *, transport: Transport, loggers, local_addr=None): + self._ip = ip + self._port = port + self._log = loggers[__name__] + self._local_addr = local_addr + + self._sock = None + self._in_buffer = bytearray() + self._transport = transport + + async def connect(self, timeout=None, ssl=None): + """ + Establishes a connection with the server. + """ + loop = asyncio.get_event_loop() + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setblocking(False) + if self._local_addr: + sock.bind(self._local_addr) + + await asyncio.wait_for(loop.sock_connect(sock, (self._ip, self._port)), timeout) + self._sock = sock + + async def disconnect(self): + self._sock.close() + self._sock = None + + async def send(self, data): + if not self._sock: + raise ConnectionError('not connected') + + loop = asyncio.get_event_loop() + await loop.sock_sendall(self._sock, self._transport.pack(data)) + + async def recv(self): + if not self._sock: + raise ConnectionError('not connected') + + loop = asyncio.get_event_loop() + while True: + try: + length, body = self._transport.unpack(self._in_buffer) + del self._in_buffer[:length] + return body + except EOFError: + self._in_buffer += await loop.sock_recv(self._sock, CHUNK_SIZE) + + def __str__(self): + return f'{self._ip}:{self._port}/{self._transport.__class__.__name__}' diff --git a/telethon/_network/connection/__init__.py b/telethon/_network/connection/__init__.py deleted file mode 100644 index 88771866..00000000 --- a/telethon/_network/connection/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from .connection import Connection -from .tcpfull import ConnectionTcpFull -from .tcpintermediate import ConnectionTcpIntermediate -from .tcpabridged import ConnectionTcpAbridged -from .tcpobfuscated import ConnectionTcpObfuscated -from .tcpmtproxy import ( - TcpMTProxy, - ConnectionTcpMTProxyAbridged, - ConnectionTcpMTProxyIntermediate, - ConnectionTcpMTProxyRandomizedIntermediate -) -from .http import ConnectionHttp diff --git a/telethon/_network/connection/connection.py b/telethon/_network/connection/connection.py deleted file mode 100644 index abf398ee..00000000 --- a/telethon/_network/connection/connection.py +++ /dev/null @@ -1,426 +0,0 @@ -import abc -import asyncio -import socket -import sys - -try: - import ssl as ssl_mod -except ImportError: - ssl_mod = None - -try: - import python_socks -except ImportError: - python_socks = None - -from ...errors._custom import InvalidChecksumError -from ..._misc import helpers - - -class Connection(abc.ABC): - """ - The `Connection` class is a wrapper around ``asyncio.open_connection``. - - Subclasses will implement different transport modes as atomic operations, - which this class eases doing since the exposed interface simply puts and - gets complete data payloads to and from queues. - - The only error that will raise from send and receive methods is - ``ConnectionError``, which will raise when attempting to send if - the client is disconnected (includes remote disconnections). - """ - # this static attribute should be redefined by `Connection` subclasses and - # should be one of `PacketCodec` implementations - packet_codec = None - - def __init__(self, ip, port, dc_id, *, loggers, proxy=None, local_addr=None): - self._ip = ip - self._port = port - self._dc_id = dc_id # only for MTProxy, it's an abstraction leak - self._log = loggers[__name__] - self._proxy = proxy - self._local_addr = local_addr - self._reader = None - self._writer = None - self._connected = False - self._send_task = None - self._recv_task = None - self._codec = None - self._obfuscation = None # TcpObfuscated and MTProxy - self._send_queue = asyncio.Queue(1) - self._recv_queue = asyncio.Queue(1) - - @staticmethod - def _wrap_socket_ssl(sock): - if ssl_mod is None: - raise RuntimeError( - 'Cannot use proxy that requires SSL ' - 'without the SSL module being available' - ) - - return ssl_mod.wrap_socket( - sock, - do_handshake_on_connect=True, - ssl_version=ssl_mod.PROTOCOL_SSLv23, - ciphers='ADH-AES256-SHA') - - @staticmethod - def _parse_proxy(proxy_type, addr, port, rdns=True, username=None, password=None): - if isinstance(proxy_type, str): - proxy_type = proxy_type.lower() - - # Always prefer `python_socks` when available - if python_socks: - from python_socks import ProxyType - - # We do the check for numerical values here - # to be backwards compatible with PySocks proxy format, - # (since socks.SOCKS5 == 2, socks.SOCKS4 == 1, socks.HTTP == 3) - if proxy_type == ProxyType.SOCKS5 or proxy_type == 2 or proxy_type == "socks5": - protocol = ProxyType.SOCKS5 - elif proxy_type == ProxyType.SOCKS4 or proxy_type == 1 or proxy_type == "socks4": - protocol = ProxyType.SOCKS4 - elif proxy_type == ProxyType.HTTP or proxy_type == 3 or proxy_type == "http": - protocol = ProxyType.HTTP - else: - raise ValueError("Unknown proxy protocol type: {}".format(proxy_type)) - - # This tuple must be compatible with `python_socks`' `Proxy.create()` signature - return protocol, addr, port, username, password, rdns - - else: - from socks import SOCKS5, SOCKS4, HTTP - - if proxy_type == 2 or proxy_type == "socks5": - protocol = SOCKS5 - elif proxy_type == 1 or proxy_type == "socks4": - protocol = SOCKS4 - elif proxy_type == 3 or proxy_type == "http": - protocol = HTTP - else: - raise ValueError("Unknown proxy protocol type: {}".format(proxy_type)) - - # This tuple must be compatible with `PySocks`' `socksocket.set_proxy()` signature - return protocol, addr, port, rdns, username, password - - async def _proxy_connect(self, timeout=None, local_addr=None): - if isinstance(self._proxy, (tuple, list)): - parsed = self._parse_proxy(*self._proxy) - elif isinstance(self._proxy, dict): - parsed = self._parse_proxy(**self._proxy) - else: - raise TypeError("Proxy of unknown format: {}".format(type(self._proxy))) - - # Always prefer `python_socks` when available - if python_socks: - # python_socks internal errors are not inherited from - # builtin IOError (just from Exception). Instead of adding those - # in exceptions clauses everywhere through the code, we - # rather monkey-patch them in place. - - python_socks._errors.ProxyError = ConnectionError - python_socks._errors.ProxyConnectionError = ConnectionError - python_socks._errors.ProxyTimeoutError = ConnectionError - - from python_socks.async_.asyncio import Proxy - - proxy = Proxy.create(*parsed) - - # WARNING: If `local_addr` is set we use manual socket creation, because, - # unfortunately, `Proxy.connect()` does not expose `local_addr` - # argument, so if we want to bind socket locally, we need to manually - # create, bind and connect socket, and then pass to `Proxy.connect()` method. - - if local_addr is None: - sock = await proxy.connect( - dest_host=self._ip, - dest_port=self._port, - timeout=timeout - ) - else: - # Here we start manual setup of the socket. - # The `address` represents the proxy ip and proxy port, - # not the destination one (!), because the socket - # connects to the proxy server, not destination server. - # IPv family is also checked on proxy address. - if ':' in proxy.proxy_host: - mode, address = socket.AF_INET6, (proxy.proxy_host, proxy.proxy_port, 0, 0) - else: - mode, address = socket.AF_INET, (proxy.proxy_host, proxy.proxy_port) - - # Create a non-blocking socket and bind it (if local address is specified). - sock = socket.socket(mode, socket.SOCK_STREAM) - sock.setblocking(False) - sock.bind(local_addr) - - # Actual TCP connection is performed here. - await asyncio.wait_for( - asyncio.get_event_loop().sock_connect(sock=sock, address=address), - timeout=timeout - ) - - # As our socket is already created and connected, - # this call sets the destination host/port and - # starts protocol negotiations with the proxy server. - sock = await proxy.connect( - dest_host=self._ip, - dest_port=self._port, - timeout=timeout, - _socket=sock - ) - - else: - import socks - - # Here `address` represents destination address (not proxy), because of - # the `PySocks` implementation of the connection routine. - # IPv family is checked on proxy address, not destination address. - if ':' in parsed[1]: - mode, address = socket.AF_INET6, (self._ip, self._port, 0, 0) - else: - mode, address = socket.AF_INET, (self._ip, self._port) - - # Setup socket, proxy, timeout and bind it (if necessary). - sock = socks.socksocket(mode, socket.SOCK_STREAM) - sock.set_proxy(*parsed) - sock.settimeout(timeout) - - if local_addr is not None: - sock.bind(local_addr) - - # Actual TCP connection and negotiation performed here. - await asyncio.wait_for( - asyncio.get_event_loop().sock_connect(sock=sock, address=address), - timeout=timeout - ) - - sock.setblocking(False) - - return sock - - async def _connect(self, timeout=None, ssl=None): - if self._local_addr is not None: - # NOTE: If port is not specified, we use 0 port - # to notify the OS that port should be chosen randomly - # from the available ones. - if isinstance(self._local_addr, tuple) and len(self._local_addr) == 2: - local_addr = self._local_addr - elif isinstance(self._local_addr, str): - local_addr = (self._local_addr, 0) - else: - raise ValueError("Unknown local address format: {}".format(self._local_addr)) - else: - local_addr = None - - if not self._proxy: - self._reader, self._writer = await asyncio.wait_for( - asyncio.open_connection( - host=self._ip, - port=self._port, - ssl=ssl, - local_addr=local_addr - ), timeout=timeout) - else: - # Proxy setup, connection and negotiation is performed here. - sock = await self._proxy_connect( - timeout=timeout, - local_addr=local_addr - ) - - # Wrap socket in SSL context (if provided) - if ssl: - sock = self._wrap_socket_ssl(sock) - - self._reader, self._writer = await asyncio.open_connection(sock=sock) - - self._codec = self.packet_codec(self) - self._init_conn() - await self._writer.drain() - - async def connect(self, timeout=None, ssl=None): - """ - Establishes a connection with the server. - """ - await self._connect(timeout=timeout, ssl=ssl) - self._connected = True - - loop = asyncio.get_event_loop() - self._send_task = loop.create_task(self._send_loop()) - self._recv_task = loop.create_task(self._recv_loop()) - - async def disconnect(self): - """ - Disconnects from the server, and clears - pending outgoing and incoming messages. - """ - self._connected = False - - await helpers._cancel( - self._log, - send_task=self._send_task, - recv_task=self._recv_task - ) - - if self._writer: - self._writer.close() - try: - await self._writer.wait_closed() - except Exception as e: - # Disconnecting should never raise. Seen: - # * OSError: No route to host and - # * OSError: [Errno 32] Broken pipe - # * ConnectionResetError - self._log.info('%s during disconnect: %s', type(e), e) - - def send(self, data): - """ - Sends a packet of data through this connection mode. - - This method returns a coroutine. - """ - if not self._connected: - raise ConnectionError('Not connected') - - return self._send_queue.put(data) - - async def recv(self): - """ - Receives a packet of data through this connection mode. - - This method returns a coroutine. - """ - while self._connected: - result = await self._recv_queue.get() - if result: # None = sentinel value = keep trying - return result - - raise ConnectionError('Not connected') - - async def _send_loop(self): - """ - This loop is constantly popping items off the queue to send them. - """ - try: - while self._connected: - self._send(await self._send_queue.get()) - await self._writer.drain() - except asyncio.CancelledError: - pass - except Exception as e: - if isinstance(e, IOError): - self._log.info('The server closed the connection while sending') - else: - self._log.exception('Unexpected exception in the send loop') - - await self.disconnect() - - async def _recv_loop(self): - """ - This loop is constantly putting items on the queue as they're read. - """ - while self._connected: - try: - data = await self._recv() - except asyncio.CancelledError: - break - except Exception as e: - if isinstance(e, (IOError, asyncio.IncompleteReadError)): - msg = 'The server closed the connection' - self._log.info(msg) - elif isinstance(e, InvalidChecksumError): - msg = 'The server response had an invalid checksum' - self._log.info(msg) - else: - msg = 'Unexpected exception in the receive loop' - self._log.exception(msg) - - await self.disconnect() - - # Add a sentinel value to unstuck recv - if self._recv_queue.empty(): - self._recv_queue.put_nowait(None) - - break - - try: - await self._recv_queue.put(data) - except asyncio.CancelledError: - break - - def _init_conn(self): - """ - This method will be called after `connect` is called. - After this method finishes, the writer will be drained. - - Subclasses should make use of this if they need to send - data to Telegram to indicate which connection mode will - be used. - """ - if self._codec.tag: - self._writer.write(self._codec.tag) - - def _send(self, data): - self._writer.write(self._codec.encode_packet(data)) - - async def _recv(self): - return await self._codec.read_packet(self._reader) - - def __str__(self): - return '{}:{}/{}'.format( - self._ip, self._port, - self.__class__.__name__.replace('Connection', '') - ) - - -class ObfuscatedConnection(Connection): - """ - Base class for "obfuscated" connections ("obfuscated2", "mtproto proxy") - """ - """ - This attribute should be redefined by subclasses - """ - obfuscated_io = None - - def _init_conn(self): - self._obfuscation = self.obfuscated_io(self) - self._writer.write(self._obfuscation.header) - - def _send(self, data): - self._obfuscation.write(self._codec.encode_packet(data)) - - async def _recv(self): - return await self._codec.read_packet(self._obfuscation) - - -class PacketCodec(abc.ABC): - """ - Base class for packet codecs - """ - - """ - This attribute should be re-defined by subclass to define if some - "magic bytes" should be sent to server right after connection is made to - signal which protocol will be used - """ - tag = None - - def __init__(self, connection): - """ - Codec is created when connection is just made. - """ - self._conn = connection - - @abc.abstractmethod - def encode_packet(self, data): - """ - Encodes single packet and returns encoded bytes. - """ - raise NotImplementedError - - @abc.abstractmethod - async def read_packet(self, reader): - """ - Reads single packet from `reader` object that should have - `readexactly(n)` method. - """ - raise NotImplementedError diff --git a/telethon/_network/connection/http.py b/telethon/_network/connection/http.py deleted file mode 100644 index e2d976f7..00000000 --- a/telethon/_network/connection/http.py +++ /dev/null @@ -1,39 +0,0 @@ -import asyncio - -from .connection import Connection, PacketCodec - - -SSL_PORT = 443 - - -class HttpPacketCodec(PacketCodec): - tag = None - obfuscate_tag = None - - def encode_packet(self, data): - return ('POST /api HTTP/1.1\r\n' - 'Host: {}:{}\r\n' - 'Content-Type: application/x-www-form-urlencoded\r\n' - 'Connection: keep-alive\r\n' - 'Keep-Alive: timeout=100000, max=10000000\r\n' - 'Content-Length: {}\r\n\r\n' - .format(self._conn._ip, self._conn._port, len(data)) - .encode('ascii') + data) - - async def read_packet(self, reader): - while True: - line = await reader.readline() - if not line or line[-1] != b'\n': - raise asyncio.IncompleteReadError(line, None) - - if line.lower().startswith(b'content-length: '): - await reader.readexactly(2) - length = int(line[16:-2]) - return await reader.readexactly(length) - - -class ConnectionHttp(Connection): - packet_codec = HttpPacketCodec - - async def connect(self, timeout=None, ssl=None): - await super().connect(timeout=timeout, ssl=self._port == SSL_PORT) diff --git a/telethon/_network/connection/tcpabridged.py b/telethon/_network/connection/tcpabridged.py deleted file mode 100644 index 171b1d8c..00000000 --- a/telethon/_network/connection/tcpabridged.py +++ /dev/null @@ -1,33 +0,0 @@ -import struct - -from .connection import Connection, PacketCodec - - -class AbridgedPacketCodec(PacketCodec): - tag = b'\xef' - obfuscate_tag = b'\xef\xef\xef\xef' - - def encode_packet(self, data): - length = len(data) >> 2 - if length < 127: - length = struct.pack('B', length) - else: - length = b'\x7f' + int.to_bytes(length, 3, 'little') - return length + data - - async def read_packet(self, reader): - length = struct.unpack('= 127: - length = struct.unpack( - ' 0: - return packet_with_padding[:-pad_size] - return packet_with_padding - - -class ConnectionTcpIntermediate(Connection): - """ - Intermediate mode between `ConnectionTcpFull` and `ConnectionTcpAbridged`. - Always sends 4 extra bytes for the packet length. - """ - packet_codec = IntermediatePacketCodec diff --git a/telethon/_network/connection/tcpmtproxy.py b/telethon/_network/connection/tcpmtproxy.py deleted file mode 100644 index db18a61c..00000000 --- a/telethon/_network/connection/tcpmtproxy.py +++ /dev/null @@ -1,152 +0,0 @@ -import asyncio -import hashlib -import os - -from .connection import ObfuscatedConnection -from .tcpabridged import AbridgedPacketCodec -from .tcpintermediate import ( - IntermediatePacketCodec, - RandomizedIntermediatePacketCodec -) - -from ..._crypto import AESModeCTR - - -class MTProxyIO: - """ - It's very similar to tcpobfuscated.ObfuscatedIO, but the way - encryption keys, protocol tag and dc_id are encoded is different. - """ - header = None - - def __init__(self, connection): - self._reader = connection._reader - self._writer = connection._writer - - (self.header, - self._encrypt, - self._decrypt) = self.init_header( - connection._secret, connection._dc_id, connection.packet_codec) - - @staticmethod - def init_header(secret, dc_id, packet_codec): - # Validate - is_dd = (len(secret) == 17) and (secret[0] == 0xDD) - is_rand_codec = issubclass( - packet_codec, RandomizedIntermediatePacketCodec) - if is_dd and not is_rand_codec: - raise ValueError( - "Only RandomizedIntermediate can be used with dd-secrets") - secret = secret[1:] if is_dd else secret - if len(secret) != 16: - raise ValueError( - "MTProxy secret must be a hex-string representing 16 bytes") - - # Obfuscated messages secrets cannot start with any of these - keywords = (b'PVrG', b'GET ', b'POST', b'\xee\xee\xee\xee') - while True: - random = os.urandom(64) - if (random[0] != 0xef and - random[:4] not in keywords and - random[4:4] != b'\0\0\0\0'): - break - - random = bytearray(random) - random_reversed = random[55:7:-1] # Reversed (8, len=48) - - # Encryption has "continuous buffer" enabled - encrypt_key = hashlib.sha256( - bytes(random[8:40]) + secret).digest() - encrypt_iv = bytes(random[40:56]) - decrypt_key = hashlib.sha256( - bytes(random_reversed[:32]) + secret).digest() - decrypt_iv = bytes(random_reversed[32:48]) - - encryptor = AESModeCTR(encrypt_key, encrypt_iv) - decryptor = AESModeCTR(decrypt_key, decrypt_iv) - - random[56:60] = packet_codec.obfuscate_tag - - dc_id_bytes = dc_id.to_bytes(2, "little", signed=True) - random = random[:60] + dc_id_bytes + random[62:] - random[56:64] = encryptor.encrypt(bytes(random))[56:64] - return (random, encryptor, decryptor) - - async def readexactly(self, n): - return self._decrypt.encrypt(await self._reader.readexactly(n)) - - def write(self, data): - self._writer.write(self._encrypt.encrypt(data)) - - -class TcpMTProxy(ObfuscatedConnection): - """ - Connector which allows user to connect to the Telegram via proxy servers - commonly known as MTProxy. - Implemented very ugly due to the leaky abstractions in Telethon networking - classes that should be refactored later (TODO). - - .. warning:: - - The support for TcpMTProxy classes is **EXPERIMENTAL** and prone to - be changed. You shouldn't be using this class yet. - """ - packet_codec = None - obfuscated_io = MTProxyIO - - # noinspection PyUnusedLocal - def __init__(self, ip, port, dc_id, *, loggers, proxy=None, local_addr=None): - # connect to proxy's host and port instead of telegram's ones - proxy_host, proxy_port = self.address_info(proxy) - self._secret = bytes.fromhex(proxy[2]) - super().__init__( - proxy_host, proxy_port, dc_id, loggers=loggers) - - async def _connect(self, timeout=None, ssl=None): - await super()._connect(timeout=timeout, ssl=ssl) - - # Wait for EOF for 2 seconds (or if _wait_for_data's definition - # is missing or different, just sleep for 2 seconds). This way - # we give the proxy a chance to close the connection if the current - # codec (which the proxy detects with the data we sent) cannot - # be used for this proxy. This is a work around for #1134. - # TODO Sleeping for N seconds may not be the best solution - # TODO This fix could be welcome for HTTP proxies as well - try: - await asyncio.wait_for(self._reader._wait_for_data('proxy'), 2) - except asyncio.TimeoutError: - pass - except Exception: - await asyncio.sleep(2) - - if self._reader.at_eof(): - await self.disconnect() - raise ConnectionError( - 'Proxy closed the connection after sending initial payload') - - @staticmethod - def address_info(proxy_info): - if proxy_info is None: - raise ValueError("No proxy info specified for MTProxy connection") - return proxy_info[:2] - - -class ConnectionTcpMTProxyAbridged(TcpMTProxy): - """ - Connect to proxy using abridged protocol - """ - packet_codec = AbridgedPacketCodec - - -class ConnectionTcpMTProxyIntermediate(TcpMTProxy): - """ - Connect to proxy using intermediate protocol - """ - packet_codec = IntermediatePacketCodec - - -class ConnectionTcpMTProxyRandomizedIntermediate(TcpMTProxy): - """ - Connect to proxy using randomized intermediate protocol (dd-secrets) - """ - packet_codec = RandomizedIntermediatePacketCodec diff --git a/telethon/_network/connection/tcpobfuscated.py b/telethon/_network/connection/tcpobfuscated.py deleted file mode 100644 index 2aeeeac1..00000000 --- a/telethon/_network/connection/tcpobfuscated.py +++ /dev/null @@ -1,62 +0,0 @@ -import os - -from .tcpabridged import AbridgedPacketCodec -from .connection import ObfuscatedConnection - -from ..._crypto import AESModeCTR - - -class ObfuscatedIO: - header = None - - def __init__(self, connection): - self._reader = connection._reader - self._writer = connection._writer - - (self.header, - self._encrypt, - self._decrypt) = self.init_header(connection.packet_codec) - - @staticmethod - def init_header(packet_codec): - # Obfuscated messages secrets cannot start with any of these - keywords = (b'PVrG', b'GET ', b'POST', b'\xee\xee\xee\xee') - while True: - random = os.urandom(64) - if (random[0] != 0xef and - random[:4] not in keywords and - random[4:8] != b'\0\0\0\0'): - break - - random = bytearray(random) - random_reversed = random[55:7:-1] # Reversed (8, len=48) - - # Encryption has "continuous buffer" enabled - encrypt_key = bytes(random[8:40]) - encrypt_iv = bytes(random[40:56]) - decrypt_key = bytes(random_reversed[:32]) - decrypt_iv = bytes(random_reversed[32:48]) - - encryptor = AESModeCTR(encrypt_key, encrypt_iv) - decryptor = AESModeCTR(decrypt_key, decrypt_iv) - - random[56:60] = packet_codec.obfuscate_tag - random[56:64] = encryptor.encrypt(bytes(random))[56:64] - return (random, encryptor, decryptor) - - async def readexactly(self, n): - return self._decrypt.encrypt(await self._reader.readexactly(n)) - - def write(self, data): - self._writer.write(self._encrypt.encrypt(data)) - - -class ConnectionTcpObfuscated(ObfuscatedConnection): - """ - Mode that Telegram defines as "obfuscated2". Encodes the packet - just like `ConnectionTcpAbridged`, but encrypts every message with - a randomly generated key using the AES-CTR mode so the packets are - harder to discern. - """ - obfuscated_io = ObfuscatedIO - packet_codec = AbridgedPacketCodec diff --git a/telethon/_network/transports/__init__.py b/telethon/_network/transports/__init__.py new file mode 100644 index 00000000..36dfc149 --- /dev/null +++ b/telethon/_network/transports/__init__.py @@ -0,0 +1,4 @@ +from .transport import Transport +from .abridged import Abridged +from .full import Full +from .intermediate import Intermediate diff --git a/telethon/_network/transports/abridged.py b/telethon/_network/transports/abridged.py new file mode 100644 index 00000000..c847c249 --- /dev/null +++ b/telethon/_network/transports/abridged.py @@ -0,0 +1,43 @@ +from .transport import Transport +import struct + + +class Abridged(Transport): + def __init__(self): + self._init = False + + def recreate_fresh(self): + return type(self)() + + def pack(self, input: bytes) -> bytes: + if self._init: + header = b'' + else: + header = b'\xef' + self._init = True + + length = len(data) >> 2 + if length < 127: + length = struct.pack('B', length) + else: + length = b'\x7f' + int.to_bytes(length, 3, 'little') + + return header + length + data + + def unpack(self, input: bytes) -> (int, bytes): + if len(input) < 4: + raise EOFError() + + length = input[0] + if length < 127: + offset = 1 + else: + offset = 4 + length = struct.unpack(' bytes: + # https://core.telegram.org/mtproto#tcp-transport + length = len(input) + 12 + data = struct.pack(' (int, bytes): + if len(input) < 12: + raise EOFError() + + length, seq = struct.unpack(' bytes: + if self._init: + header = b'' + else: + header = b'\xee\xee\xee\xee' + self._init = True + + return header + struct.pack(' (int, bytes): + if len(input) < 4: + raise EOFError() + + length = struct.unpack(' bytes: + pass + + # Should raise EOFError if it does not have enough bytes + @abc.abstractmethod + def unpack(self, input: bytes) -> (int, bytes): + pass From fe941cb940d44de0444cb51febbe20a8db9badbc Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 15 Jan 2022 13:41:17 +0100 Subject: [PATCH 133/256] Address immutability issues on connect --- telethon/_client/telegrambaseclient.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index e4906fbf..8ed1a599 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -7,6 +7,7 @@ import platform import time import typing import ipaddress +import dataclasses from .. import version, __name__ as __base_name__, _tl from .._crypto import rsa @@ -342,7 +343,7 @@ async def connect(self: 'TelegramClient') -> None: return if self._sender.auth_key.key != dc.auth: - dc.auth = self._sender.auth_key.key + dc = dataclasses.replace(dc, auth=self._sender.auth_key.key) # Need to send invokeWithLayer for things to work out. # Make the most out of this opportunity by also refreshing our state. @@ -359,11 +360,10 @@ async def connect(self: 'TelegramClient') -> None: ip = int(ipaddress.ip_address(dc.ip_address)) if dc.id in self._all_dcs: - self._all_dcs[dc.id].port = dc.port if dc.ipv6: - self._all_dcs[dc.id].ipv6 = ip + self._all_dcs[dc.id] = dataclasses.replace(self._all_dcs[dc.id], port=dc.port, ipv6=ip) else: - self._all_dcs[dc.id].ipv4 = ip + self._all_dcs[dc.id] = dataclasses.replace(self._all_dcs[dc.id], port=dc.port, ipv4=ip) elif dc.ipv6: self._all_dcs[dc.id] = DataCenter(dc.id, None, ip, dc.port, b'') else: From 3ad6d86cf52a7e5ece9cea8a6a8fd352cd6f357a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 16 Jan 2022 11:48:30 +0100 Subject: [PATCH 134/256] Update to layer 137 --- telethon_generator/data/api.tl | 144 ++++++++++++++++++++++----------- 1 file changed, 98 insertions(+), 46 deletions(-) diff --git a/telethon_generator/data/api.tl b/telethon_generator/data/api.tl index d9b3d1d7..9aaa0e57 100644 --- a/telethon_generator/data/api.tl +++ b/telethon_generator/data/api.tl @@ -123,13 +123,13 @@ userStatusLastWeek#7bf09fc = UserStatus; userStatusLastMonth#77ebc742 = UserStatus; chatEmpty#29562865 id:long = Chat; -chat#41cbf256 flags:# creator:flags.0?true kicked:flags.1?true left:flags.2?true deactivated:flags.5?true call_active:flags.23?true call_not_empty:flags.24?true id:long title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel admin_rights:flags.14?ChatAdminRights default_banned_rights:flags.18?ChatBannedRights = Chat; +chat#41cbf256 flags:# creator:flags.0?true kicked:flags.1?true left:flags.2?true deactivated:flags.5?true call_active:flags.23?true call_not_empty:flags.24?true noforwards:flags.25?true id:long title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel admin_rights:flags.14?ChatAdminRights default_banned_rights:flags.18?ChatBannedRights = Chat; chatForbidden#6592a1a7 id:long title:string = Chat; -channel#8261ac61 flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true fake:flags.25?true gigagroup:flags.26?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int restriction_reason:flags.9?Vector admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int = Chat; +channel#8261ac61 flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int restriction_reason:flags.9?Vector admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int = Chat; channelForbidden#17d493d5 flags:# broadcast:flags.5?true megagroup:flags.8?true id:long access_hash:long title:string until_date:flags.16?int = Chat; -chatFull#4dbdc099 flags:# can_set_username:flags.7?true has_scheduled:flags.8?true id:long about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer theme_emoticon:flags.16?string = ChatFull; -channelFull#e9b27a17 flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string = ChatFull; +chatFull#d18ee226 flags:# can_set_username:flags.7?true has_scheduled:flags.8?true id:long about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer theme_emoticon:flags.16?string requests_pending:flags.17?int recent_requesters:flags.17?Vector available_reactions:flags.18?Vector = ChatFull; +channelFull#e13c3d20 flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector default_send_as:flags.29?Peer available_reactions:flags.30?Vector = ChatFull; chatParticipant#c02d4007 user_id:long inviter_id:long date:int = ChatParticipant; chatParticipantCreator#e46bcee4 user_id:long = ChatParticipant; @@ -142,7 +142,7 @@ chatPhotoEmpty#37c1011c = ChatPhoto; chatPhoto#1c6e1c11 flags:# has_video:flags.0?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = ChatPhoto; messageEmpty#90a6ca84 flags:# id:int peer_id:flags.0?Peer = Message; -message#85d6cbe2 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true id:int from_id:flags.8?Peer peer_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long restriction_reason:flags.22?Vector ttl_period:flags.25?int = Message; +message#38116ee0 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true id:int from_id:flags.8?Peer peer_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector ttl_period:flags.25?int = Message; messageService#2b085862 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction ttl_period:flags.25?int = Message; messageMediaEmpty#3ded6320 = MessageMedia; @@ -188,6 +188,7 @@ messageActionInviteToGroupCall#502f92f7 call:InputGroupCall users:Vector = messageActionSetMessagesTTL#aa1afbfd period:int = MessageAction; messageActionGroupCallScheduled#b3a07661 call:InputGroupCall schedule_date:int = MessageAction; messageActionSetChatTheme#aa786345 emoticon:string = MessageAction; +messageActionChatJoinedByRequest#ebbca3cb = MessageAction; dialog#2c171f72 flags:# pinned:flags.2?true unread_mark:flags.3?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int = Dialog; dialogFolder#71bd134c flags:# pinned:flags.2?true folder:Folder peer:Peer top_message:int unread_muted_peers_count:int unread_unmuted_peers_count:int unread_muted_messages_count:int unread_unmuted_messages_count:int = Dialog; @@ -207,7 +208,7 @@ geoPoint#b2a2f663 flags:# long:double lat:double access_hash:long accuracy_radiu auth.sentCode#5e002502 flags:# type:auth.SentCodeType phone_code_hash:string next_type:flags.1?auth.CodeType timeout:flags.2?int = auth.SentCode; -auth.authorization#cd050916 flags:# tmp_sessions:flags.0?int user:User = auth.Authorization; +auth.authorization#33fb7bb8 flags:# setup_password_required:flags.1?true otherwise_relogin_days:flags.1?int tmp_sessions:flags.0?int user:User = auth.Authorization; auth.authorizationSignUpRequired#44747e9a flags:# terms_of_service:flags.0?help.TermsOfService = auth.Authorization; auth.exportedAuthorization#b434e2b8 id:long bytes:bytes = auth.ExportedAuthorization; @@ -221,7 +222,7 @@ inputPeerNotifySettings#9c3d198e flags:# show_previews:flags.0?Bool silent:flags peerNotifySettings#af509d20 flags:# show_previews:flags.0?Bool silent:flags.1?Bool mute_until:flags.2?int sound:flags.3?string = PeerNotifySettings; -peerSettings#733f2961 flags:# report_spam:flags.0?true add_contact:flags.1?true block_contact:flags.2?true share_contact:flags.3?true need_contacts_exception:flags.4?true report_geo:flags.5?true autoarchived:flags.7?true invite_members:flags.8?true geo_distance:flags.6?int = PeerSettings; +peerSettings#a518110d flags:# report_spam:flags.0?true add_contact:flags.1?true block_contact:flags.2?true share_contact:flags.3?true need_contacts_exception:flags.4?true report_geo:flags.5?true autoarchived:flags.7?true invite_members:flags.8?true request_chat_broadcast:flags.10?true geo_distance:flags.6?int request_chat_title:flags.9?string request_chat_date:flags.9?int = PeerSettings; wallPaper#a437c3ed id:long flags:# creator:flags.0?true default:flags.1?true pattern:flags.3?true dark:flags.4?true access_hash:long slug:string document:Document settings:flags.2?WallPaperSettings = WallPaper; wallPaperNoFile#e0804116 id:long flags:# default:flags.1?true dark:flags.4?true settings:flags.2?WallPaperSettings = WallPaper; @@ -235,7 +236,7 @@ inputReportReasonCopyright#9b89f93a = ReportReason; inputReportReasonGeoIrrelevant#dbd4feed = ReportReason; inputReportReasonFake#f5ddd6e7 = ReportReason; -userFull#d697ff05 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true user:User about:flags.1?string settings:PeerSettings profile_photo:flags.2?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string = UserFull; +userFull#cf366521 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true id:long about:flags.1?string settings:PeerSettings profile_photo:flags.2?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string = UserFull; contact#145ade0b user_id:long mutual:Bool = Contact; @@ -378,6 +379,9 @@ updateChannelParticipant#985d3abb flags:# channel_id:long date:int actor_id:long updateBotStopped#c4870a49 user_id:long date:int stopped:Bool qts:int = Update; updateGroupCallConnection#b783982 flags:# presentation:flags.0?true params:DataJSON = Update; updateBotCommands#4d712f2e peer:Peer bot_id:long commands:Vector = Update; +updatePendingJoinRequests#7063c3db peer:Peer requests_pending:int recent_requesters:Vector = Update; +updateBotChatInviteRequester#11dfa986 peer:Peer date:int user_id:long about:string invite:ExportedChatInvite qts:int = Update; +updateMessageReactions#154798c3 peer:Peer msg_id:int reactions:MessageReactions = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -467,7 +471,7 @@ sendMessageUploadRoundAction#243e1c66 progress:int = SendMessageAction; speakingInGroupCallAction#d92c2285 = SendMessageAction; sendMessageHistoryImportAction#dbda9246 progress:int = SendMessageAction; sendMessageChooseStickerAction#b05ac6b1 = SendMessageAction; -sendMessageEmojiInteraction#6a3233b6 emoticon:string interaction:DataJSON = SendMessageAction; +sendMessageEmojiInteraction#25972bcb emoticon:string msg_id:int interaction:DataJSON = SendMessageAction; sendMessageEmojiInteractionSeen#b665902e emoticon:string = SendMessageAction; contacts.found#b3134d9d my_results:Vector results:Vector chats:Vector users:Vector = contacts.Found; @@ -535,9 +539,9 @@ webPagePending#c586da1c id:long date:int = WebPage; webPage#e89c45b2 flags:# id:long url:string display_url:string hash:int type:flags.0?string site_name:flags.1?string title:flags.2?string description:flags.3?string photo:flags.4?Photo embed_url:flags.5?string embed_type:flags.5?string embed_width:flags.6?int embed_height:flags.6?int duration:flags.7?int author:flags.8?string document:flags.9?Document cached_page:flags.10?Page attributes:flags.12?Vector = WebPage; webPageNotModified#7311ca11 flags:# cached_page_views:flags.0?int = WebPage; -authorization#ad01d61d flags:# current:flags.0?true official_app:flags.1?true password_pending:flags.2?true hash:long device_model:string platform:string system_version:string api_id:int app_name:string app_version:string date_created:int date_active:int ip:string country:string region:string = Authorization; +authorization#ad01d61d flags:# current:flags.0?true official_app:flags.1?true password_pending:flags.2?true encrypted_requests_disabled:flags.3?true call_requests_disabled:flags.4?true hash:long device_model:string platform:string system_version:string api_id:int app_name:string app_version:string date_created:int date_active:int ip:string country:string region:string = Authorization; -account.authorizations#1250abde authorizations:Vector = account.Authorizations; +account.authorizations#4bff8ea0 authorization_ttl_days:int authorizations:Vector = account.Authorizations; account.password#185b184f flags:# has_recovery:flags.0?true has_secure_values:flags.1?true has_password:flags.2?true current_algo:flags.2?PasswordKdfAlgo srp_B:flags.2?bytes srp_id:flags.2?long hint:flags.3?string email_unconfirmed_pattern:flags.4?string new_algo:PasswordKdfAlgo new_secure_algo:SecurePasswordKdfAlgo secure_random:bytes pending_reset_date:flags.5?int = account.Password; @@ -549,10 +553,10 @@ auth.passwordRecovery#137948a5 email_pattern:string = auth.PasswordRecovery; receivedNotifyMessage#a384b779 id:int flags:int = ReceivedNotifyMessage; -chatInviteExported#b18105e8 flags:# revoked:flags.0?true permanent:flags.5?true link:string admin_id:long date:int start_date:flags.4?int expire_date:flags.1?int usage_limit:flags.2?int usage:flags.3?int = ExportedChatInvite; +chatInviteExported#ab4a819 flags:# revoked:flags.0?true permanent:flags.5?true request_needed:flags.6?true link:string admin_id:long date:int start_date:flags.4?int expire_date:flags.1?int usage_limit:flags.2?int usage:flags.3?int requested:flags.7?int title:flags.8?string = ExportedChatInvite; chatInviteAlready#5a686d7c chat:Chat = ChatInvite; -chatInvite#dfc2f58e flags:# channel:flags.0?true broadcast:flags.1?true public:flags.2?true megagroup:flags.3?true title:string photo:Photo participants_count:int participants:flags.4?Vector = ChatInvite; +chatInvite#300c44c1 flags:# channel:flags.0?true broadcast:flags.1?true public:flags.2?true megagroup:flags.3?true request_needed:flags.6?true title:string about:flags.5?string photo:Photo participants_count:int participants:flags.4?Vector = ChatInvite; chatInvitePeek#61695cb0 chat:Chat expires:int = ChatInvite; inputStickerSetEmpty#ffb62b95 = InputStickerSet; @@ -560,10 +564,12 @@ inputStickerSetID#9de7a269 id:long access_hash:long = InputStickerSet; inputStickerSetShortName#861cc8a0 short_name:string = InputStickerSet; inputStickerSetAnimatedEmoji#28703c8 = InputStickerSet; inputStickerSetDice#e67f520e emoticon:string = InputStickerSet; +inputStickerSetAnimatedEmojiAnimations#cde3739 = InputStickerSet; stickerSet#d7df217a flags:# archived:flags.1?true official:flags.2?true masks:flags.3?true animated:flags.5?true installed_date:flags.0?int id:long access_hash:long title:string short_name:string thumbs:flags.4?Vector thumb_dc_id:flags.4?int thumb_version:flags.4?int count:int hash:int = StickerSet; messages.stickerSet#b60a24a6 set:StickerSet packs:Vector documents:Vector = messages.StickerSet; +messages.stickerSetNotModified#d3f924eb = messages.StickerSet; botCommand#c27ac8c7 command:string description:string = BotCommand; @@ -580,6 +586,8 @@ keyboardButtonBuy#afd93fbb text:string = KeyboardButton; keyboardButtonUrlAuth#10b78d29 flags:# text:string fwd_text:flags.0?string url:string button_id:int = KeyboardButton; inputKeyboardButtonUrlAuth#d02e7fd4 flags:# request_write_access:flags.0?true text:string fwd_text:flags.1?string url:string bot:InputUser = KeyboardButton; keyboardButtonRequestPoll#bbc7515d flags:# quiz:flags.0?Bool text:string = KeyboardButton; +inputKeyboardButtonUserProfile#e988037b text:string user_id:InputUser = KeyboardButton; +keyboardButtonUserProfile#308660c1 text:string user_id:long = KeyboardButton; keyboardButtonRow#77608b83 buttons:Vector = KeyboardButtonRow; @@ -607,6 +615,7 @@ messageEntityUnderline#9c4e7e8b offset:int length:int = MessageEntity; messageEntityStrike#bf0693d4 offset:int length:int = MessageEntity; messageEntityBlockquote#20df5d0 offset:int length:int = MessageEntity; messageEntityBankCard#761e6af4 offset:int length:int = MessageEntity; +messageEntitySpoiler#32ca960f offset:int length:int = MessageEntity; inputChannelEmpty#ee8c1e86 = InputChannel; inputChannel#f35aec28 channel_id:long access_hash:long = InputChannel; @@ -624,7 +633,7 @@ channelMessagesFilterEmpty#94d42ee7 = ChannelMessagesFilter; channelMessagesFilter#cd77d957 flags:# exclude_new_messages:flags.1?true ranges:Vector = ChannelMessagesFilter; channelParticipant#c00c07c0 user_id:long date:int = ChannelParticipant; -channelParticipantSelf#28a8bc67 user_id:long inviter_id:long date:int = ChannelParticipant; +channelParticipantSelf#35a8bfa7 flags:# via_request:flags.0?true user_id:long inviter_id:long date:int = ChannelParticipant; channelParticipantCreator#2fe601d3 flags:# user_id:long admin_rights:ChatAdminRights rank:flags.0?string = ChannelParticipant; channelParticipantAdmin#34c3bb53 flags:# can_edit:flags.0?true self:flags.1?true user_id:long inviter_id:flags.1?long promoted_by:long date:int admin_rights:ChatAdminRights rank:flags.2?string = ChannelParticipant; channelParticipantBanned#6df8014e flags:# left:flags.0?true peer:Peer kicked_by:long date:int banned_rights:ChatBannedRights = ChannelParticipant; @@ -681,11 +690,13 @@ messageFwdHeader#5f777dce flags:# imported:flags.7?true from_id:flags.0?Peer fro auth.codeTypeSms#72a3158c = auth.CodeType; auth.codeTypeCall#741cd3e3 = auth.CodeType; auth.codeTypeFlashCall#226ccefb = auth.CodeType; +auth.codeTypeMissedCall#d61ad6ee = auth.CodeType; auth.sentCodeTypeApp#3dbb5986 length:int = auth.SentCodeType; auth.sentCodeTypeSms#c000bba2 length:int = auth.SentCodeType; auth.sentCodeTypeCall#5353e5a7 length:int = auth.SentCodeType; auth.sentCodeTypeFlashCall#ab03c6d9 pattern:string = auth.SentCodeType; +auth.sentCodeTypeMissedCall#82006484 prefix:string length:int = auth.SentCodeType; messages.botCallbackAnswer#36585ea4 flags:# alert:flags.1?true has_url:flags.3?true native_ui:flags.4?true message:flags.0?string url:flags.2?string cache_time:int = messages.BotCallbackAnswer; @@ -907,13 +918,16 @@ channelAdminLogEventActionExportedInviteRevoke#410a134e invite:ExportedChatInvit channelAdminLogEventActionExportedInviteEdit#e90ebb59 prev_invite:ExportedChatInvite new_invite:ExportedChatInvite = ChannelAdminLogEventAction; channelAdminLogEventActionParticipantVolume#3e7f6847 participant:GroupCallParticipant = ChannelAdminLogEventAction; channelAdminLogEventActionChangeHistoryTTL#6e941a38 prev_value:int new_value:int = ChannelAdminLogEventAction; -channelAdminLogEventActionChangeTheme#fe69018d prev_value:string new_value:string = ChannelAdminLogEventAction; +channelAdminLogEventActionParticipantJoinByRequest#afb6144a invite:ExportedChatInvite approved_by:long = ChannelAdminLogEventAction; +channelAdminLogEventActionToggleNoForwards#cb2ac766 new_value:Bool = ChannelAdminLogEventAction; +channelAdminLogEventActionSendMessage#278f2868 message:Message = ChannelAdminLogEventAction; +channelAdminLogEventActionChangeAvailableReactions#9cf7f76a prev_value:Vector new_value:Vector = ChannelAdminLogEventAction; channelAdminLogEvent#1fad68cd id:long date:int user_id:long action:ChannelAdminLogEventAction = ChannelAdminLogEvent; channels.adminLogResults#ed8af74d events:Vector chats:Vector users:Vector = channels.AdminLogResults; -channelAdminLogEventsFilter#ea107ae4 flags:# join:flags.0?true leave:flags.1?true invite:flags.2?true ban:flags.3?true unban:flags.4?true kick:flags.5?true unkick:flags.6?true promote:flags.7?true demote:flags.8?true info:flags.9?true settings:flags.10?true pinned:flags.11?true edit:flags.12?true delete:flags.13?true group_call:flags.14?true invites:flags.15?true = ChannelAdminLogEventsFilter; +channelAdminLogEventsFilter#ea107ae4 flags:# join:flags.0?true leave:flags.1?true invite:flags.2?true ban:flags.3?true unban:flags.4?true kick:flags.5?true unkick:flags.6?true promote:flags.7?true demote:flags.8?true info:flags.9?true settings:flags.10?true pinned:flags.11?true edit:flags.12?true delete:flags.13?true group_call:flags.14?true invites:flags.15?true send:flags.16?true = ChannelAdminLogEventsFilter; popularContact#5ce14175 client_id:long importers:int = PopularContact; @@ -1082,7 +1096,7 @@ inputWallPaperNoFile#967a462e id:long = InputWallPaper; account.wallPapersNotModified#1c199183 = account.WallPapers; account.wallPapers#cdc3858c hash:long wallpapers:Vector = account.WallPapers; -codeSettings#debebe83 flags:# allow_flashcall:flags.0?true current_number:flags.1?true allow_app_hash:flags.4?true = CodeSettings; +codeSettings#8a6469c2 flags:# allow_flashcall:flags.0?true current_number:flags.1?true allow_app_hash:flags.4?true allow_missed_call:flags.5?true logout_tokens:flags.6?Vector = CodeSettings; wallPaperSettings#1dc1bca4 flags:# blur:flags.1?true motion:flags.2?true background_color:flags.0?int second_background_color:flags.4?int third_background_color:flags.5?int fourth_background_color:flags.6?int intensity:flags.3?int rotation:flags.4?int = WallPaperSettings; @@ -1122,7 +1136,7 @@ restrictionReason#d072acb4 platform:string reason:string text:string = Restricti inputTheme#3c5693e9 id:long access_hash:long = InputTheme; inputThemeSlug#f5890df1 slug:string = InputTheme; -theme#e802b8dc flags:# creator:flags.0?true default:flags.1?true for_chat:flags.5?true id:long access_hash:long slug:string title:string document:flags.2?Document settings:flags.3?ThemeSettings installs_count:flags.4?int = Theme; +theme#a00e67d6 flags:# creator:flags.0?true default:flags.1?true for_chat:flags.5?true id:long access_hash:long slug:string title:string document:flags.2?Document settings:flags.3?Vector emoticon:flags.6?string installs_count:flags.4?int = Theme; account.themesNotModified#f41eb622 = account.Themes; account.themes#9a3d8c6d hash:long themes:Vector = account.Themes; @@ -1234,7 +1248,7 @@ messages.historyImportParsed#5e0fb7b9 flags:# pm:flags.0?true group:flags.1?true messages.affectedFoundMessages#ef8d3e6c pts:int pts_count:int offset:int messages:Vector = messages.AffectedFoundMessages; -chatInviteImporter#b5cd5f4 user_id:long date:int = ChatInviteImporter; +chatInviteImporter#8c5adfd9 flags:# requested:flags.0?true user_id:long date:int about:flags.2?string approved_by:flags.1?long = ChatInviteImporter; messages.exportedChatInvites#bdc62dcc count:int invites:Vector users:Vector = messages.ExportedChatInvites; @@ -1271,15 +1285,39 @@ account.resetPasswordFailedWait#e3779861 retry_date:int = account.ResetPasswordR account.resetPasswordRequestedWait#e9effc7d until_date:int = account.ResetPasswordResult; account.resetPasswordOk#e926d63e = account.ResetPasswordResult; -chatTheme#ed0b5c33 emoticon:string theme:Theme dark_theme:Theme = ChatTheme; - -account.chatThemesNotModified#e011e1c4 = account.ChatThemes; -account.chatThemes#fe4cbebd hash:int themes:Vector = account.ChatThemes; - -sponsoredMessage#2a3c381f flags:# random_id:bytes from_id:Peer start_param:flags.0?string message:string entities:flags.1?Vector = SponsoredMessage; +sponsoredMessage#3a836df8 flags:# random_id:bytes from_id:flags.3?Peer chat_invite:flags.4?ChatInvite chat_invite_hash:flags.4?string channel_post:flags.2?int start_param:flags.0?string message:string entities:flags.1?Vector = SponsoredMessage; messages.sponsoredMessages#65a4c7d5 messages:Vector chats:Vector users:Vector = messages.SponsoredMessages; +searchResultsCalendarPeriod#c9b0539f date:int min_msg_id:int max_msg_id:int count:int = SearchResultsCalendarPeriod; + +messages.searchResultsCalendar#147ee23c flags:# inexact:flags.0?true count:int min_date:int min_msg_id:int offset_id_offset:flags.1?int periods:Vector messages:Vector chats:Vector users:Vector = messages.SearchResultsCalendar; + +searchResultPosition#7f648b67 msg_id:int date:int offset:int = SearchResultsPosition; + +messages.searchResultsPositions#53b22baf count:int positions:Vector = messages.SearchResultsPositions; + +channels.sendAsPeers#8356cda9 peers:Vector chats:Vector users:Vector = channels.SendAsPeers; + +users.userFull#3b6d152e full_user:UserFull chats:Vector users:Vector = users.UserFull; + +messages.peerSettings#6880b94d settings:PeerSettings chats:Vector users:Vector = messages.PeerSettings; + +auth.loggedOut#c3a2835f flags:# future_auth_token:flags.0?bytes = auth.LoggedOut; + +reactionCount#6fb250d1 flags:# chosen:flags.0?true reaction:string count:int = ReactionCount; + +messageReactions#87b6e36 flags:# min:flags.0?true can_see_list:flags.2?true results:Vector recent_reactons:flags.1?Vector = MessageReactions; + +messageUserReaction#932844fa user_id:long reaction:string = MessageUserReaction; + +messages.messageReactionsList#a366923c flags:# count:int reactions:Vector users:Vector next_offset:flags.0?string = messages.MessageReactionsList; + +availableReaction#c077ec01 flags:# inactive:flags.0?true reaction:string title:string static_icon:Document appear_animation:Document select_animation:Document activate_animation:Document effect_animation:Document around_animation:flags.1?Document center_icon:flags.1?Document = AvailableReaction; + +messages.availableReactionsNotModified#9f071957 = messages.AvailableReactions; +messages.availableReactions#768e3aad hash:int reactions:Vector = messages.AvailableReactions; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1293,7 +1331,7 @@ invokeWithTakeout#aca9fd2e {X:Type} takeout_id:long query:!X = X; auth.sendCode#a677244f phone_number:string api_id:int api_hash:string settings:CodeSettings = auth.SentCode; auth.signUp#80eee427 phone_number:string phone_code_hash:string first_name:string last_name:string = auth.Authorization; auth.signIn#bcd51581 phone_number:string phone_code_hash:string phone_code:string = auth.Authorization; -auth.logOut#5717da40 = Bool; +auth.logOut#3e72ba19 = auth.LoggedOut; auth.resetAuthorizations#9fab0d1a = Bool; auth.exportAuthorization#e5bfffcd dc_id:int = auth.ExportedAuthorization; auth.importAuthorization#a57a7dad id:long bytes:bytes = auth.Authorization; @@ -1366,10 +1404,10 @@ account.resetWallPapers#bb3b9804 = Bool; account.getAutoDownloadSettings#56da0b3f = account.AutoDownloadSettings; account.saveAutoDownloadSettings#76f36233 flags:# low:flags.0?true high:flags.1?true settings:AutoDownloadSettings = Bool; account.uploadTheme#1c3db333 flags:# file:InputFile thumb:flags.0?InputFile file_name:string mime_type:string = Document; -account.createTheme#8432c21f flags:# slug:string title:string document:flags.2?InputDocument settings:flags.3?InputThemeSettings = Theme; -account.updateTheme#5cb367d5 flags:# format:string theme:InputTheme slug:flags.0?string title:flags.1?string document:flags.2?InputDocument settings:flags.3?InputThemeSettings = Theme; +account.createTheme#652e4400 flags:# slug:string title:string document:flags.2?InputDocument settings:flags.3?Vector = Theme; +account.updateTheme#2bf40ccc flags:# format:string theme:InputTheme slug:flags.0?string title:flags.1?string document:flags.2?InputDocument settings:flags.3?Vector = Theme; account.saveTheme#f257106c theme:InputTheme unsave:Bool = Bool; -account.installTheme#7ae43737 flags:# dark:flags.0?true format:flags.1?string theme:flags.1?InputTheme = Bool; +account.installTheme#c727bb3b flags:# dark:flags.0?true theme:flags.1?InputTheme format:flags.2?string base_theme:flags.3?BaseTheme = Bool; account.getTheme#8d9d742b format:string theme:InputTheme document_id:long = Theme; account.getThemes#7206e458 format:string hash:long = account.Themes; account.setContentSettings#b574b16b flags:# sensitive_enabled:flags.0?true = Bool; @@ -1380,10 +1418,12 @@ account.setGlobalPrivacySettings#1edaaac2 settings:GlobalPrivacySettings = Globa account.reportProfilePhoto#fa8cc6f5 peer:InputPeer photo_id:InputPhoto reason:ReportReason message:string = Bool; account.resetPassword#9308ce1b = account.ResetPasswordResult; account.declinePasswordReset#4c9409f6 = Bool; -account.getChatThemes#d6d71d7b hash:int = account.ChatThemes; +account.getChatThemes#d638de89 hash:long = account.Themes; +account.setAuthorizationTTL#bf899aa0 authorization_ttl_days:int = Bool; +account.changeAuthorizationSettings#40f48462 flags:# hash:long encrypted_requests_disabled:flags.0?Bool call_requests_disabled:flags.1?Bool = Bool; users.getUsers#d91a548 id:Vector = Vector; -users.getFullUser#ca30a5b1 id:InputUser = UserFull; +users.getFullUser#b60f5918 id:InputUser = users.UserFull; users.setSecureValueErrors#90c894b5 id:InputUser errors:Vector = Bool; contacts.getContactIDs#7adc669d hash:long = Vector; @@ -1412,15 +1452,15 @@ messages.getDialogs#a0f4cb4f flags:# exclude_pinned:flags.0?true folder_id:flags messages.getHistory#4423e6c5 peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages; messages.search#a0fda762 flags:# peer:InputPeer q:string from_id:flags.0?InputPeer top_msg_id:flags.1?int filter:MessagesFilter min_date:int max_date:int offset_id:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages; messages.readHistory#e306d3a peer:InputPeer max_id:int = messages.AffectedMessages; -messages.deleteHistory#1c015b09 flags:# just_clear:flags.0?true revoke:flags.1?true peer:InputPeer max_id:int = messages.AffectedHistory; +messages.deleteHistory#b08f922a flags:# just_clear:flags.0?true revoke:flags.1?true peer:InputPeer max_id:int min_date:flags.2?int max_date:flags.3?int = messages.AffectedHistory; messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector = messages.AffectedMessages; messages.receivedMessages#5a954c0 max_id:int = Vector; messages.setTyping#58943ee2 flags:# peer:InputPeer top_msg_id:flags.0?int action:SendMessageAction = Bool; -messages.sendMessage#520c3870 flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int = Updates; -messages.sendMedia#3491eba9 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int = Updates; -messages.forwardMessages#d9fee60e flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true from_peer:InputPeer id:Vector random_id:Vector to_peer:InputPeer schedule_date:flags.10?int = Updates; +messages.sendMessage#d9d75a4 flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true peer:InputPeer reply_to_msg_id:flags.0?int message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; +messages.sendMedia#e25ff8e0 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true peer:InputPeer reply_to_msg_id:flags.0?int media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; +messages.forwardMessages#cc30290b flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true from_peer:InputPeer id:Vector random_id:Vector to_peer:InputPeer schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; messages.reportSpam#cf1592db peer:InputPeer = Bool; -messages.getPeerSettings#3672e09c peer:InputPeer = PeerSettings; +messages.getPeerSettings#efd9a6a2 peer:InputPeer = messages.PeerSettings; messages.report#8953ab4e peer:InputPeer id:Vector reason:ReportReason message:string = Bool; messages.getChats#49e9528f id:Vector = messages.Chats; messages.getFullChat#aeb00b34 chat_id:long = messages.ChatFull; @@ -1444,10 +1484,10 @@ messages.readMessageContents#36a73f77 id:Vector = messages.AffectedMessages messages.getStickers#d5a5d3a1 emoticon:string hash:long = messages.Stickers; messages.getAllStickers#b8a0a1a8 hash:long = messages.AllStickers; messages.getWebPagePreview#8b68b0cc flags:# message:string entities:flags.3?Vector = MessageMedia; -messages.exportChatInvite#14b9bcd7 flags:# legacy_revoke_permanent:flags.2?true peer:InputPeer expire_date:flags.0?int usage_limit:flags.1?int = ExportedChatInvite; +messages.exportChatInvite#a02ce5d5 flags:# legacy_revoke_permanent:flags.2?true request_needed:flags.3?true peer:InputPeer expire_date:flags.0?int usage_limit:flags.1?int title:flags.4?string = ExportedChatInvite; messages.checkChatInvite#3eadb1bb hash:string = ChatInvite; messages.importChatInvite#6c50051c hash:string = Updates; -messages.getStickerSet#2619a90e stickerset:InputStickerSet = messages.StickerSet; +messages.getStickerSet#c8a0ec74 stickerset:InputStickerSet hash:int = messages.StickerSet; messages.installStickerSet#c78fe460 stickerset:InputStickerSet archived:Bool = messages.StickerSetInstallResult; messages.uninstallStickerSet#f96e55de stickerset:InputStickerSet = Bool; messages.startBot#e6df7378 bot:InputUser peer:InputPeer random_id:long start_param:string = Updates; @@ -1461,7 +1501,7 @@ messages.getSavedGifs#5cf09635 hash:long = messages.SavedGifs; messages.saveGif#327a30cb id:InputDocument unsave:Bool = Bool; messages.getInlineBotResults#514e999d flags:# bot:InputUser peer:InputPeer geo_point:flags.0?InputGeoPoint query:string offset:string = messages.BotResults; messages.setInlineBotResults#eb5ea206 flags:# gallery:flags.0?true private:flags.1?true query_id:long results:Vector cache_time:int next_offset:flags.2?string switch_pm:flags.3?InlineBotSwitchPM = Bool; -messages.sendInlineBotResult#220815b0 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true hide_via:flags.11?true peer:InputPeer reply_to_msg_id:flags.0?int random_id:long query_id:long id:string schedule_date:flags.10?int = Updates; +messages.sendInlineBotResult#7aa11297 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true hide_via:flags.11?true peer:InputPeer reply_to_msg_id:flags.0?int random_id:long query_id:long id:string schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; messages.getMessageEditData#fda68d36 peer:InputPeer id:int = messages.MessageEditData; messages.editMessage#48f71778 flags:# no_webpage:flags.1?true peer:InputPeer id:int message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.15?int = Updates; messages.editInlineBotMessage#83557dba flags:# no_webpage:flags.1?true id:InputBotInlineMessageID message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector = Bool; @@ -1497,7 +1537,7 @@ messages.faveSticker#b9ffc55b id:InputDocument unfave:Bool = Bool; messages.getUnreadMentions#46578472 peer:InputPeer offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; messages.readMentions#f0189d3 peer:InputPeer = messages.AffectedHistory; messages.getRecentLocations#702a40e0 peer:InputPeer limit:int hash:long = messages.Messages; -messages.sendMultiMedia#cc0110cb flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int multi_media:Vector schedule_date:flags.10?int = Updates; +messages.sendMultiMedia#f803138f flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true peer:InputPeer reply_to_msg_id:flags.0?int multi_media:Vector schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; messages.uploadEncryptedFile#5057c497 peer:InputEncryptedChat file:InputEncryptedFile = EncryptedFile; messages.searchStickerSets#35705b8a flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets; messages.getSplitRanges#1cff7e08 = Vector; @@ -1508,7 +1548,6 @@ messages.updatePinnedMessage#d2aaf7ec flags:# silent:flags.0?true unpin:flags.1? messages.sendVote#10ea6184 peer:InputPeer msg_id:int options:Vector = Updates; messages.getPollResults#73bb643b peer:InputPeer msg_id:int = Updates; messages.getOnlines#6e2be050 peer:InputPeer = ChatOnlines; -messages.getStatsURL#812c2ae6 flags:# dark:flags.0?true peer:InputPeer params:string = StatsURL; messages.editChatAbout#def60797 peer:InputPeer about:string = Bool; messages.editChatDefaultBannedRights#a5866b41 peer:InputPeer banned_rights:ChatBannedRights = Updates; messages.getEmojiKeywords#35a0e062 lang_code:string = EmojiKeywordsDifference; @@ -1542,15 +1581,27 @@ messages.uploadImportedMedia#2a862092 peer:InputPeer import_id:long file_name:st messages.startHistoryImport#b43df344 peer:InputPeer import_id:long = Bool; messages.getExportedChatInvites#a2b5a3f6 flags:# revoked:flags.3?true peer:InputPeer admin_id:InputUser offset_date:flags.2?int offset_link:flags.2?string limit:int = messages.ExportedChatInvites; messages.getExportedChatInvite#73746f5c peer:InputPeer link:string = messages.ExportedChatInvite; -messages.editExportedChatInvite#2e4ffbe flags:# revoked:flags.2?true peer:InputPeer link:string expire_date:flags.0?int usage_limit:flags.1?int = messages.ExportedChatInvite; +messages.editExportedChatInvite#bdca2f75 flags:# revoked:flags.2?true peer:InputPeer link:string expire_date:flags.0?int usage_limit:flags.1?int request_needed:flags.3?Bool title:flags.4?string = messages.ExportedChatInvite; messages.deleteRevokedExportedChatInvites#56987bd5 peer:InputPeer admin_id:InputUser = Bool; messages.deleteExportedChatInvite#d464a42b peer:InputPeer link:string = Bool; messages.getAdminsWithInvites#3920e6ef peer:InputPeer = messages.ChatAdminsWithInvites; -messages.getChatInviteImporters#26fb7289 peer:InputPeer link:string offset_date:int offset_user:InputUser limit:int = messages.ChatInviteImporters; +messages.getChatInviteImporters#df04dd4e flags:# requested:flags.0?true peer:InputPeer link:flags.1?string q:flags.2?string offset_date:int offset_user:InputUser limit:int = messages.ChatInviteImporters; messages.setHistoryTTL#b80e5fe4 peer:InputPeer period:int = Updates; messages.checkHistoryImportPeer#5dc60f03 peer:InputPeer = messages.CheckedHistoryImportPeer; messages.setChatTheme#e63be13f peer:InputPeer emoticon:string = Updates; messages.getMessageReadParticipants#2c6f97b7 peer:InputPeer msg_id:int = Vector; +messages.getSearchResultsCalendar#49f0bde9 peer:InputPeer filter:MessagesFilter offset_id:int offset_date:int = messages.SearchResultsCalendar; +messages.getSearchResultsPositions#6e9583a3 peer:InputPeer filter:MessagesFilter offset_id:int limit:int = messages.SearchResultsPositions; +messages.hideChatJoinRequest#7fe7e815 flags:# approved:flags.0?true peer:InputPeer user_id:InputUser = Updates; +messages.hideAllChatJoinRequests#e085f4ea flags:# approved:flags.0?true peer:InputPeer link:flags.1?string = Updates; +messages.toggleNoForwards#b11eafa2 peer:InputPeer enabled:Bool = Updates; +messages.saveDefaultSendAs#ccfddf96 peer:InputPeer send_as:InputPeer = Bool; +messages.sendReaction#25690ce4 flags:# peer:InputPeer msg_id:int reaction:flags.0?string = Updates; +messages.getMessagesReactions#8bba90e6 peer:InputPeer id:Vector = Updates; +messages.getMessageReactionsList#e0ee6b77 flags:# peer:InputPeer id:int reaction:flags.0?string offset:flags.1?string limit:int = messages.MessageReactionsList; +messages.setChatAvailableReactions#14050ea6 peer:InputPeer available_reactions:Vector = Updates; +messages.getAvailableReactions#18dea0ac hash:int = messages.AvailableReactions; +messages.setDefaultReaction#d960c4d4 reaction:string = Bool; updates.getState#edd4882a = updates.State; updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference; @@ -1595,8 +1646,7 @@ help.getCountriesList#735787a8 lang_code:string hash:int = help.CountriesList; channels.readHistory#cc104937 channel:InputChannel max_id:int = Bool; channels.deleteMessages#84c1fd4e channel:InputChannel id:Vector = messages.AffectedMessages; -channels.deleteUserHistory#d10dd71b channel:InputChannel user_id:InputUser = messages.AffectedHistory; -channels.reportSpam#fe087810 channel:InputChannel user_id:InputUser id:Vector = Bool; +channels.reportSpam#f44a8315 channel:InputChannel participant:InputPeer id:Vector = Bool; channels.getMessages#ad8c9a23 channel:InputChannel id:Vector = messages.Messages; channels.getParticipants#77ced9d0 channel:InputChannel filter:ChannelParticipantsFilter offset:int limit:int hash:long = channels.ChannelParticipants; channels.getParticipant#a0ab6cc6 channel:InputChannel participant:InputPeer = channels.ChannelParticipant; @@ -1631,6 +1681,8 @@ channels.getInactiveChannels#11e831ee = messages.InactiveChats; channels.convertToGigagroup#b290c69 channel:InputChannel = Updates; channels.viewSponsoredMessage#beaedb94 channel:InputChannel random_id:bytes = Bool; channels.getSponsoredMessages#ec210fbf channel:InputChannel = messages.SponsoredMessages; +channels.getSendAs#dc770ee peer:InputPeer = channels.SendAsPeers; +channels.deleteParticipantHistory#367544db channel:InputChannel participant:InputPeer = messages.AffectedHistory; bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON; bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool; @@ -1698,4 +1750,4 @@ stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel stats.getMessagePublicForwards#5630281b channel:InputChannel msg_id:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats; -// LAYER 133 +// LAYER 137 From 8c9ee3f73152919d24e50a79d10c6af1af1b9f27 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 16 Jan 2022 12:01:18 +0100 Subject: [PATCH 135/256] Document new known errors --- telethon_generator/data/errors.csv | 6 ++++++ telethon_generator/data/methods.csv | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/telethon_generator/data/errors.csv b/telethon_generator/data/errors.csv index 71a417d8..5ef81e85 100644 --- a/telethon_generator/data/errors.csv +++ b/telethon_generator/data/errors.csv @@ -66,6 +66,7 @@ CHAT_ABOUT_TOO_LONG,400,Chat about too long CHAT_ADMIN_INVITE_REQUIRED,403,You do not have the rights to do this CHAT_ADMIN_REQUIRED,400,"Chat admin privileges are required to do that in the specified chat (for example, to send a message in a channel which is not yours), or invalid permissions used for the channel or group" CHAT_FORBIDDEN,403,You cannot write in this chat +CHAT_FORWARDS_RESTRICTED,, CHAT_ID_EMPTY,400,The provided chat ID is empty CHAT_ID_INVALID,400,"Invalid object ID for a chat. Make sure to pass the right types, for instance making sure that the request is designed for chats (not channels/megagroups) or otherwise look for a different one more suited\nAn example working with a megagroup and AddChatUserRequest, it will fail because megagroups are channels. Use InviteToChannelRequest instead" CHAT_INVALID,400,The chat is invalid for this request @@ -116,6 +117,7 @@ ENCRYPTION_OCCUPY_FAILED,500,TDLib developer claimed it is not an error while ac ENTITIES_TOO_LONG,400,It is no longer possible to send such long data inside entity tags (for example inline text URLs) ENTITY_MENTION_USER_INVALID,400,You can't use this entity ERROR_TEXT_EMPTY,400,The provided error message is empty +EXPIRE_DATE_INVALID,400, EXPIRE_FORBIDDEN,400, EXPORT_CARD_INVALID,400,Provided card is invalid EXTERNAL_URL_INVALID,400,External URL invalid @@ -230,6 +232,7 @@ PACK_SHORT_NAME_OCCUPIED,400,A stickerpack with this name already exists PARTICIPANTS_TOO_FEW,400,Not enough participants PARTICIPANT_CALL_FAILED,500,Failure while making call PARTICIPANT_JOIN_MISSING,403, +PARTICIPANT_ID_INVALID,, PARTICIPANT_VERSION_OUTDATED,400,The other participant does not use an up to date telegram client with support for calls PASSWORD_EMPTY,400,The provided password is empty PASSWORD_HASH_INVALID,400,The password (and thus its hash value) you entered is invalid @@ -315,6 +318,8 @@ SCHEDULE_STATUS_PRIVATE,400,You cannot schedule a message until the person comes SCHEDULE_TOO_MUCH,400,You cannot schedule more messages in this chat (last known limit of 100 per chat) SEARCH_QUERY_EMPTY,400,The search query is empty SECONDS_INVALID,400,"Slow mode only supports certain values (e.g. 0, 10s, 30s, 1m, 5m, 15m and 1h)" +SEND_AS_PEER_INVALID,, +SEND_CODE_UNAVAILABLE,406, SEND_MESSAGE_MEDIA_INVALID,400,The message media was invalid or not specified SEND_MESSAGE_TYPE_INVALID,400,The message type is invalid SENSITIVE_CHANGE_FORBIDDEN,403,Your sensitive content settings cannot be changed at this time @@ -366,6 +371,7 @@ TYPE_CONSTRUCTOR_INVALID,400,The type constructor is invalid UNKNOWN_ERROR,400, UNKNOWN_METHOD,500,The method you tried to call cannot be called on non-CDN DCs UNTIL_DATE_INVALID,400,That date cannot be specified in this request (try using None) +UPDATE_APP_TO_LOGIN,406, URL_INVALID,400,The URL used was invalid (e.g. when answering a callback with a URL that's not t.me/yourbot or your game's URL) USER_VOLUME_INVALID,400, USERNAME_INVALID,400,"Nobody is using this username, or the username is unacceptable. If the latter, it must match r""[a-zA-Z][\w\d]{3,30}[a-zA-Z\d]""" diff --git a/telethon_generator/data/methods.csv b/telethon_generator/data/methods.csv index 135d5618..c8c92ab5 100644 --- a/telethon_generator/data/methods.csv +++ b/telethon_generator/data/methods.csv @@ -80,7 +80,7 @@ auth.importLoginToken,user,AUTH_TOKEN_ALREADY_ACCEPTED AUTH_TOKEN_EXPIRED AUTH_T auth.logOut,both, auth.recoverPassword,user,CODE_EMPTY NEW_SETTINGS_INVALID auth.requestPasswordRecovery,user,PASSWORD_EMPTY PASSWORD_RECOVERY_NA -auth.resendCode,user,PHONE_NUMBER_INVALID +auth.resendCode,user,PHONE_NUMBER_INVALID SEND_CODE_UNAVAILABLE auth.resetAuthorizations,user,TIMEOUT auth.sendCode,user,API_ID_INVALID API_ID_PUBLISHED_FLOOD AUTH_RESTART INPUT_REQUEST_TOO_LONG PHONE_NUMBER_APP_SIGNUP_FORBIDDEN PHONE_NUMBER_BANNED PHONE_NUMBER_FLOOD PHONE_NUMBER_INVALID PHONE_PASSWORD_FLOOD PHONE_PASSWORD_PROTECTED auth.signIn,user,PHONE_CODE_EMPTY PHONE_CODE_EXPIRED PHONE_CODE_INVALID PHONE_NUMBER_INVALID PHONE_NUMBER_UNOCCUPIED SESSION_PASSWORD_NEEDED @@ -197,7 +197,7 @@ messages.editChatPhoto,both,CHAT_ID_INVALID INPUT_CONSTRUCTOR_INVALID INPUT_FETC messages.editChatTitle,both,CHAT_ID_INVALID NEED_CHAT_INVALID messages.editInlineBotMessage,both,MESSAGE_ID_INVALID MESSAGE_NOT_MODIFIED messages.editMessage,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_WRITE_FORBIDDEN INLINE_BOT_REQUIRED INPUT_USER_DEACTIVATED MEDIA_GROUPED_INVALID MEDIA_NEW_INVALID MEDIA_PREV_INVALID MESSAGE_AUTHOR_REQUIRED MESSAGE_EDIT_TIME_EXPIRED MESSAGE_EMPTY MESSAGE_ID_INVALID MESSAGE_NOT_MODIFIED PEER_ID_INVALID -messages.exportChatInvite,both,CHAT_ID_INVALID +messages.exportChatInvite,both,CHAT_ID_INVALID EXPIRE_DATE_INVALID messages.faveSticker,user,STICKER_ID_INVALID messages.forwardMessages,both,BROADCAST_PUBLIC_VOTERS_FORBIDDEN CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_ID_INVALID CHAT_SEND_GIFS_FORBIDDEN CHAT_SEND_MEDIA_FORBIDDEN CHAT_SEND_STICKERS_FORBIDDEN CHAT_WRITE_FORBIDDEN GROUPED_MEDIA_INVALID INPUT_USER_DEACTIVATED MEDIA_EMPTY MESSAGE_IDS_EMPTY MESSAGE_ID_INVALID PEER_ID_INVALID PTS_CHANGE_EMPTY RANDOM_ID_DUPLICATE RANDOM_ID_INVALID SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH TIMEOUT USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT YOU_BLOCKED_USER messages.getAllChats,user, From a95393648f0878b32919ba7a399eaf1a44927aea Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 16 Jan 2022 12:06:42 +0100 Subject: [PATCH 136/256] Remove custom enum parsing Python enums can already be parsed out-of-the-box. --- telethon/_client/chats.py | 32 +++++++++++++------------- telethon/_client/downloads.py | 6 ++--- telethon/_client/telegrambaseclient.py | 2 +- telethon/_misc/enums.py | 22 ------------------ 4 files changed, 20 insertions(+), 42 deletions(-) diff --git a/telethon/_client/chats.py b/telethon/_client/chats.py index 3034b5d9..acba307e 100644 --- a/telethon/_client/chats.py +++ b/telethon/_client/chats.py @@ -69,21 +69,21 @@ class _ChatAction: return action return { - enums.TYPING: _tl.SendMessageTypingAction(), - enums.CONTACT: _tl.SendMessageChooseContactAction(), - enums.GAME: _tl.SendMessageGamePlayAction(), - enums.LOCATION: _tl.SendMessageGeoLocationAction(), - enums.STICKER: _tl.SendMessageChooseStickerAction(), - enums.RECORD_AUDIO: _tl.SendMessageRecordAudioAction(), - enums.RECORD_ROUND: _tl.SendMessageRecordRoundAction(), - enums.RECORD_VIDEO: _tl.SendMessageRecordVideoAction(), - enums.AUDIO: _tl.SendMessageUploadAudioAction(1), - enums.ROUND: _tl.SendMessageUploadRoundAction(1), - enums.VIDEO: _tl.SendMessageUploadVideoAction(1), - enums.PHOTO: _tl.SendMessageUploadPhotoAction(1), - enums.DOCUMENT: _tl.SendMessageUploadDocumentAction(1), - enums.CANCEL: _tl.SendMessageCancelAction(), - }[enums.parse_typing_action(action)] + enums.Action.TYPING: _tl.SendMessageTypingAction(), + enums.Action.CONTACT: _tl.SendMessageChooseContactAction(), + enums.Action.GAME: _tl.SendMessageGamePlayAction(), + enums.Action.LOCATION: _tl.SendMessageGeoLocationAction(), + enums.Action.STICKER: _tl.SendMessageChooseStickerAction(), + enums.Action.RECORD_AUDIO: _tl.SendMessageRecordAudioAction(), + enums.Action.RECORD_ROUND: _tl.SendMessageRecordRoundAction(), + enums.Action.RECORD_VIDEO: _tl.SendMessageRecordVideoAction(), + enums.Action.AUDIO: _tl.SendMessageUploadAudioAction(1), + enums.Action.ROUND: _tl.SendMessageUploadRoundAction(1), + enums.Action.VIDEO: _tl.SendMessageUploadVideoAction(1), + enums.Action.PHOTO: _tl.SendMessageUploadPhotoAction(1), + enums.Action.DOCUMENT: _tl.SendMessageUploadDocumentAction(1), + enums.Action.CANCEL: _tl.SendMessageCancelAction(), + }[enums.Action(action)] def progress(self, current, total): if hasattr(self._action, 'progress'): @@ -98,7 +98,7 @@ class _ParticipantsIter(requestiter.RequestIter): else: filter = _tl.ChannelParticipantsRecent() else: - filter = enums.parse_participant(filter) + filter = enums.Participant(filter) if filter == enums.Participant.ADMIN: filter = _tl.ChannelParticipantsAdmins() elif filter == enums.Participant.BOT: diff --git a/telethon/_client/downloads.py b/telethon/_client/downloads.py index 6339b8c9..d472dbcf 100644 --- a/telethon/_client/downloads.py +++ b/telethon/_client/downloads.py @@ -210,7 +210,7 @@ async def download_profile_photo( photo = entity.photo if isinstance(photo, (_tl.UserProfilePhoto, _tl.ChatPhoto)): - thumb = enums.Size.ORIGINAL if thumb == () else enums.parse_photo_size(thumb) + thumb = enums.Size.ORIGINAL if thumb == () else enums.Size(thumb) dc_id = photo.dc_id loc = _tl.InputPeerPhotoFileLocation( @@ -494,11 +494,11 @@ def _get_thumb(thumbs, thumb): if isinstance(thumb, tlobject.TLObject): return thumb - thumb = enums.parse_photo_size(thumb) + thumb = enums.Size(thumb) return min( thumbs, default=None, - key=lambda t: abs(thumb - enums.parse_photo_size(t.type)) + key=lambda t: abs(thumb - enums.Size(t.type)) ) def _download_cached_photo_size(self: 'TelegramClient', size, file): diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 8ed1a599..10cd2d7f 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -199,7 +199,7 @@ def init( enums.ConnectionMode.FULL: transports.Full(), enums.ConnectionMode.INTERMEDIATE: transports.Intermediate(), enums.ConnectionMode.ABRIDGED: transports.Abridged(), - }[enums.parse_conn_mode(connection)] + }[enums.ConnectionMode(connection)] init_proxy = None # Used on connection. Capture the variables in a lambda since diff --git a/telethon/_misc/enums.py b/telethon/_misc/enums.py index 283030f3..79b20242 100644 --- a/telethon/_misc/enums.py +++ b/telethon/_misc/enums.py @@ -129,25 +129,3 @@ class Size(Enum): Size.ANIMATED: 7, Size.VIDEO: 6, }[self] - - -def _mk_parser(cls): - def parser(value): - if isinstance(value, cls): - return value - elif isinstance(value, str): - for variant in cls: - if value == variant.value: - return variant - - raise ValueError(f'unknown {cls.__name__}: {value!r}') - else: - raise TypeError(f'not a valid {cls.__name__}: {type(value).__name__!r}') - - return parser - - -parse_conn_mode = _mk_parser(ConnectionMode) -parse_participant = _mk_parser(Participant) -parse_typing_action = _mk_parser(Action) -parse_photo_size = _mk_parser(Size) From a3513d52321a8aba23a5b094b0a0874aa915dda8 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 16 Jan 2022 12:19:07 +0100 Subject: [PATCH 137/256] Remove broken force_sms --- readthedocs/misc/v2-migration-guide.rst | 2 ++ telethon/_client/auth.py | 43 +++++++++---------------- telethon/_client/telegramclient.py | 12 +------ 3 files changed, 19 insertions(+), 38 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 97b0bcb2..7d047e2e 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -748,3 +748,5 @@ still thumb because otherwise documents are weird. keep support for explicit size instance? renamed send_read_acknowledge. add send_read_acknowledge as alias for mark_read? + +force sms removed as it was broken anyway and not very reliable diff --git a/telethon/_client/auth.py b/telethon/_client/auth.py index 50df992b..90bb5ae5 100644 --- a/telethon/_client/auth.py +++ b/telethon/_client/auth.py @@ -37,7 +37,6 @@ def start( password: typing.Callable[[], str] = lambda: getpass.getpass('Please enter your password: '), *, bot_token: str = None, - force_sms: bool = False, code_callback: typing.Callable[[], typing.Union[str, int]] = None, first_name: str = 'New User', last_name: str = '', @@ -63,7 +62,6 @@ def start( phone=phone, password=password, bot_token=bot_token, - force_sms=force_sms, code_callback=code_callback, first_name=first_name, last_name=last_name, @@ -71,7 +69,7 @@ def start( )) async def _start( - self: 'TelegramClient', phone, password, bot_token, force_sms, + self: 'TelegramClient', phone, password, bot_token, code_callback, first_name, last_name, max_attempts): if not self.is_connected(): await self.connect() @@ -123,7 +121,7 @@ async def _start( attempts = 0 two_step_detected = False - await self.send_code_request(phone, force_sms=force_sms) + await self.send_code_request(phone) sign_up = False # assume login while attempts < max_attempts: try: @@ -343,37 +341,28 @@ async def _replace_session_state(self, *, save=True, **changes): async def send_code_request( self: 'TelegramClient', - phone: str, - *, - force_sms: bool = False) -> '_tl.auth.SentCode': + phone: str) -> '_tl.auth.SentCode': result = None phone = utils.parse_phone(phone) or self._phone phone_hash = self._phone_code_hash.get(phone) - if not phone_hash: - try: - result = await self(_tl.fn.auth.SendCode( - phone, self.api_id, self.api_hash, _tl.CodeSettings())) - except errors.AuthRestartError: - return await self.send_code_request(phone, force_sms=force_sms) - - # If we already sent a SMS, do not resend the code (hash may be empty) - if isinstance(result.type, _tl.auth.SentCodeTypeSms): - force_sms = False - - # phone_code_hash may be empty, if it is, do not save it (#1283) - if result.phone_code_hash: - self._phone_code_hash[phone] = phone_hash = result.phone_code_hash - else: - force_sms = True - - self._phone = phone - - if force_sms: + if phone_hash: result = await self( _tl.fn.auth.ResendCode(phone, phone_hash)) self._phone_code_hash[phone] = result.phone_code_hash + else: + try: + result = await self(_tl.fn.auth.SendCode( + phone, self.api_id, self.api_hash, _tl.CodeSettings())) + except errors.AuthRestartError: + return await self.send_code_request(phone) + + # phone_code_hash may be empty, if it is, do not save it (#1283) + if result.phone_code_hash: + self._phone_code_hash[phone] = phone_hash = result.phone_code_hash + + self._phone = phone return result diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index bb21a416..79e51404 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -321,7 +321,6 @@ class TelegramClient: password: typing.Callable[[], str] = lambda: getpass.getpass('Please enter your password: '), *, bot_token: str = None, - force_sms: bool = False, code_callback: typing.Callable[[], typing.Union[str, int]] = None, first_name: str = 'New User', last_name: str = '', @@ -358,10 +357,6 @@ class TelegramClient: to log in as a bot. Cannot be specified with ``phone`` (only one of either allowed). - force_sms (`bool`, optional): - Whether to force sending the code request as SMS. - This only makes sense when signing in with a `phone`. - code_callback (`callable`, optional): A callable that will be used to retrieve the Telegram login code. Defaults to `input()`. @@ -513,9 +508,7 @@ class TelegramClient: @forward_call(auth.send_code_request) async def send_code_request( self: 'TelegramClient', - phone: str, - *, - force_sms: bool = False) -> '_tl.auth.SentCode': + phone: str) -> '_tl.auth.SentCode': """ Sends the Telegram code needed to login to the given phone number. @@ -523,9 +516,6 @@ class TelegramClient: phone (`str` | `int`): The phone to which the code will be sent. - force_sms (`bool`, optional): - Whether to force sending as SMS. - Returns An instance of :tl:`SentCode`. From dc0f978b59836f91edd59d344489454f8f2fdbf9 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 16 Jan 2022 12:40:09 +0100 Subject: [PATCH 138/256] Support await on any client.action --- readthedocs/misc/v2-migration-guide.rst | 2 ++ telethon/_client/chats.py | 12 +++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 7d047e2e..91ec043d 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -750,3 +750,5 @@ keep support for explicit size instance? renamed send_read_acknowledge. add send_read_acknowledge as alias for mark_read? force sms removed as it was broken anyway and not very reliable + +you can now await client.action for a one-off any action not just cancel diff --git a/telethon/_client/chats.py b/telethon/_client/chats.py index acba307e..f900ce6d 100644 --- a/telethon/_client/chats.py +++ b/telethon/_client/chats.py @@ -27,6 +27,9 @@ class _ChatAction: self._task = None self._running = False + def __await__(self): + return self._once().__await__() + async def __aenter__(self): self._chat = await self._client.get_input_entity(self._chat) @@ -51,6 +54,10 @@ class _ChatAction: self._task = None + async def _once(self): + self._chat = await self._client.get_input_entity(self._chat) + await self._client(_tl.fn.messages.SetTyping(self._chat, self._action)) + async def _update(self): try: while self._running: @@ -456,11 +463,6 @@ def action( auto_cancel: bool = True) -> 'typing.Union[_ChatAction, typing.Coroutine]': action = _ChatAction._parse(action) - if isinstance(action, _tl.SendMessageCancelAction): - # ``SetTyping.resolve`` will get input peer of ``entity``. - return self(_tl.fn.messages.SetTyping( - entity, _tl.SendMessageCancelAction())) - return _ChatAction( self, entity, action, delay=delay, auto_cancel=auto_cancel) From 1e779a91b72cce36075c9b32d0786067b7f17eec Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 16 Jan 2022 12:42:00 +0100 Subject: [PATCH 139/256] Add progress_callback to download_profile_photo --- telethon/_client/downloads.py | 7 ++++--- telethon/_client/telegramclient.py | 7 ++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/telethon/_client/downloads.py b/telethon/_client/downloads.py index d472dbcf..0b9f9aaf 100644 --- a/telethon/_client/downloads.py +++ b/telethon/_client/downloads.py @@ -180,7 +180,8 @@ async def download_profile_photo( entity: 'hints.EntityLike', file: 'hints.FileLike' = None, *, - thumb) -> typing.Optional[str]: + thumb, + progress_callback) -> typing.Optional[str]: # hex(crc32(x.encode('ascii'))) for x in # ('User', 'Chat', 'UserFull', 'ChatFull') ENTITIES = (0x2da17977, 0xc5af5d94, 0x1f4661b9, 0xd49a2697) @@ -201,7 +202,7 @@ async def download_profile_photo( return await _download_photo( self, entity.chat_photo, file, date=None, - thumb=thumb, progress_callback=None + thumb=thumb, progress_callback=progress_callback ) for attr in ('username', 'first_name', 'title'): @@ -247,7 +248,7 @@ async def download_profile_photo( full = await self(_tl.fn.channels.GetFullChannel(ie)) return await _download_photo( self, full.full_chat.chat_photo, file, - date=None, progress_callback=None, + date=None, progress_callback=progress_callback, thumb=thumb ) else: diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 79e51404..c4b9467b 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -1628,7 +1628,8 @@ class TelegramClient: entity: 'hints.EntityLike', file: 'hints.FileLike' = None, *, - thumb: typing.Union[str, enums.Size] = ()) -> typing.Optional[str]: + thumb: typing.Union[str, enums.Size] = (), + progress_callback: 'hints.ProgressCallback' = None) -> typing.Optional[str]: """ Downloads the profile photo from the given user, chat or channel. @@ -1662,6 +1663,10 @@ class TelegramClient: By default, the largest size (original) is downloaded. + progress_callback (`callable`, optional): + A callback function accepting two parameters: + ``(received bytes, total)``. + Returns `None` if no photo was provided, or if it was Empty. On success the file path is returned since it may differ from the one given. From 6eadc8aed815184563734d3fb14c39fc42a77311 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 16 Jan 2022 13:03:00 +0100 Subject: [PATCH 140/256] Simplify accepted values in forward, delete and mark read Forward and delete are meant to delete lists. Now only lists are supported, which should not be an issue as message.forward_to and message.delete both exist. mark_read really only works with one message at a time, so list support was removed for it, as well as the now redundant max_id. --- readthedocs/misc/v2-migration-guide.rst | 7 ++++ telethon/_client/messages.py | 51 +++++++++---------------- telethon/_client/telegramclient.py | 24 ++++++------ 3 files changed, 38 insertions(+), 44 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 91ec043d..10556cc0 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -752,3 +752,10 @@ renamed send_read_acknowledge. add send_read_acknowledge as alias for mark_read? force sms removed as it was broken anyway and not very reliable you can now await client.action for a one-off any action not just cancel + +fwd msg and delete msg now mandate a list rather than a single int or msg +(since there's msg.delete and msg.forward_to this should be no issue). +they are meant to work on lists. + +also mark read only supports single now. a list would just be max anyway. +removed max id since it's not really of much use. diff --git a/telethon/_client/messages.py b/telethon/_client/messages.py index f55fed8d..e85921f3 100644 --- a/telethon/_client/messages.py +++ b/telethon/_client/messages.py @@ -518,7 +518,7 @@ async def send_message( async def forward_messages( self: 'TelegramClient', entity: 'hints.EntityLike', - messages: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]', + messages: 'typing.Union[typing.Sequence[hints.MessageIDLike]]', from_peer: 'hints.EntityLike' = None, *, background: bool = None, @@ -530,10 +530,6 @@ async def forward_messages( if as_album is not None: warnings.warn('the as_album argument is deprecated and no longer has any effect') - single = not utils.is_list_like(messages) - if single: - messages = (messages,) - entity = await self.get_input_entity(entity) if from_peer: @@ -639,16 +635,13 @@ async def edit_message( async def delete_messages( self: 'TelegramClient', entity: 'hints.EntityLike', - message_ids: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]', + messages: 'typing.Union[typing.Sequence[hints.MessageIDLike]]', *, revoke: bool = True) -> 'typing.Sequence[_tl.messages.AffectedMessages]': - if not utils.is_list_like(message_ids): - message_ids = (message_ids,) - - message_ids = ( + messages = ( m.id if isinstance(m, ( _tl.Message, _tl.MessageService, _tl.MessageEmpty)) - else int(m) for m in message_ids + else int(m) for m in messages ) if entity: @@ -660,42 +653,36 @@ async def delete_messages( if ty == helpers._EntityType.CHANNEL: res = await self([_tl.fn.channels.DeleteMessages( - entity, list(c)) for c in utils.chunks(message_ids)]) + entity, list(c)) for c in utils.chunks(messages)]) else: res = await self([_tl.fn.messages.DeleteMessages( - list(c), revoke) for c in utils.chunks(message_ids)]) + list(c), revoke) for c in utils.chunks(messages)]) return sum(r.pts_count for r in res) async def mark_read( self: 'TelegramClient', entity: 'hints.EntityLike', - message: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]' = None, + message: 'hints.MessageIDLike' = None, *, - max_id: int = None, clear_mentions: bool = False) -> bool: - if max_id is None: - if not message: - max_id = 0 - else: - if utils.is_list_like(message): - max_id = max(msg.id for msg in message) - else: - max_id = message.id + if not message: + max_id = 0 + elif isinstance(message, int): + max_id = message + else: + max_id = message.id entity = await self.get_input_entity(entity) if clear_mentions: await self(_tl.fn.messages.ReadMentions(entity)) - if max_id is None: - return True - if max_id is not None: - if helpers._entity_type(entity) == helpers._EntityType.CHANNEL: - return await self(_tl.fn.channels.ReadHistory( - utils.get_input_channel(entity), max_id=max_id)) - else: - return await self(_tl.fn.messages.ReadHistory( - entity, max_id=max_id)) + if helpers._entity_type(entity) == helpers._EntityType.CHANNEL: + return await self(_tl.fn.channels.ReadHistory( + utils.get_input_channel(entity), max_id=max_id)) + else: + return await self(_tl.fn.messages.ReadHistory( + entity, max_id=max_id)) return False diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index c4b9467b..d5a2fab7 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -2254,7 +2254,7 @@ class TelegramClient: async def forward_messages( self: 'TelegramClient', entity: 'hints.EntityLike', - messages: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]', + messages: 'typing.Union[typing.Sequence[hints.MessageIDLike]]', from_peer: 'hints.EntityLike' = None, *, background: bool = None, @@ -2276,8 +2276,8 @@ class TelegramClient: entity (`entity`): To which entity the message(s) will be forwarded. - messages (`list` | `int` | `Message `): - The message(s) to forward, or their integer IDs. + messages (`list`): + The messages to forward, or their integer IDs. from_peer (`entity`): If the given messages are integer IDs and not instances @@ -2465,7 +2465,7 @@ class TelegramClient: async def delete_messages( self: 'TelegramClient', entity: 'hints.EntityLike', - message_ids: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]', + messages: 'typing.Union[typing.Sequence[hints.MessageIDLike]]', *, revoke: bool = True) -> 'typing.Sequence[_tl.messages.AffectedMessages]': """ @@ -2486,8 +2486,8 @@ class TelegramClient: be `None` for normal chats, but **must** be present for channels and megagroups. - message_ids (`list` | `int` | `Message `): - The IDs (or ID) or messages to be deleted. + messages (`list`): + The messages to delete, or their integer IDs. revoke (`bool`, optional): Whether the message should be deleted for everyone or not. @@ -2517,9 +2517,8 @@ class TelegramClient: async def mark_read( self: 'TelegramClient', entity: 'hints.EntityLike', - message: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]' = None, + message: 'hints.MessageIDLike' = None, *, - max_id: int = None, clear_mentions: bool = False) -> bool: """ Marks messages as read and optionally clears mentions. @@ -2527,8 +2526,8 @@ class TelegramClient: This effectively marks a message as read (or more than one) in the given conversation. - If neither message nor maximum ID are provided, all messages will be - marked as read by assuming that ``max_id = 0``. + If no message or maximum ID is provided, all messages will be + marked as read. If a message or maximum ID is provided, all the messages up to and including such ID will be marked as read (for all messages whose ID @@ -2540,8 +2539,9 @@ class TelegramClient: entity (`entity`): The chat where these messages are located. - message (`list` | `Message `): - Either a list of messages or a single message. + message (`Message `): + The last (most-recent) message which was read, or its ID. + This is only useful if you want to mark a chat as partially read. max_id (`int`): Until which message should the read acknowledge be sent for. From a62627534ec2a3581ae46ca604f3f3be0d24b797 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 16 Jan 2022 13:51:23 +0100 Subject: [PATCH 141/256] Get rid of client.loop Instead, use the asyncio-intended way of implicit loop. --- readthedocs/basic/quick-start.rst | 96 +++++++++---------- readthedocs/basic/signing-in.rst | 15 ++- readthedocs/concepts/full-api.rst | 2 +- readthedocs/concepts/sessions.rst | 4 +- readthedocs/misc/v2-migration-guide.rst | 2 + readthedocs/modules/client.rst | 6 +- telethon/_client/chats.py | 2 +- telethon/_client/telegrambaseclient.py | 25 +---- telethon/_client/telegramclient.py | 24 ----- telethon/_client/updates.py | 4 +- telethon/_events/album.py | 8 +- telethon/_events/callbackquery.py | 49 ++++++---- telethon/_events/inlinequery.py | 2 +- telethon/_network/connection.py | 8 +- telethon/_network/mtprotosender.py | 13 ++- telethon_examples/gui.py | 10 +- .../interactive_telegram_client.py | 30 +++--- telethon_examples/payment.py | 4 +- telethon_examples/quart_login.py | 13 +-- 19 files changed, 140 insertions(+), 177 deletions(-) diff --git a/readthedocs/basic/quick-start.rst b/readthedocs/basic/quick-start.rst index 8dbf928d..bd36b048 100644 --- a/readthedocs/basic/quick-start.rst +++ b/readthedocs/basic/quick-start.rst @@ -8,70 +8,70 @@ use these if possible. .. code-block:: python + import asyncio from telethon import TelegramClient # Remember to use your own values from my.telegram.org! api_id = 12345 api_hash = '0123456789abcdef0123456789abcdef' - client = TelegramClient('anon', api_id, api_hash) async def main(): - # Getting information about yourself - me = await client.get_me() + async with TelegramClient('anon', api_id, api_hash).start() as client: + # Getting information about yourself + me = await client.get_me() - # "me" is a user object. You can pretty-print - # any Telegram object with the "stringify" method: - print(me.stringify()) + # "me" is a user object. You can pretty-print + # any Telegram object with the "stringify" method: + print(me.stringify()) - # When you print something, you see a representation of it. - # You can access all attributes of Telegram objects with - # the dot operator. For example, to get the username: - username = me.username - print(username) - print(me.phone) + # When you print something, you see a representation of it. + # You can access all attributes of Telegram objects with + # the dot operator. For example, to get the username: + username = me.username + print(username) + print(me.phone) - # You can print all the dialogs/conversations that you are part of: - async for dialog in client.iter_dialogs(): - print(dialog.name, 'has ID', dialog.id) + # You can print all the dialogs/conversations that you are part of: + async for dialog in client.iter_dialogs(): + print(dialog.name, 'has ID', dialog.id) - # You can send messages to yourself... - await client.send_message('me', 'Hello, myself!') - # ...to some chat ID - await client.send_message(-100123456, 'Hello, group!') - # ...to your contacts - await client.send_message('+34600123123', 'Hello, friend!') - # ...or even to any username - await client.send_message('username', 'Testing Telethon!') + # You can send messages to yourself... + await client.send_message('me', 'Hello, myself!') + # ...to some chat ID + await client.send_message(-100123456, 'Hello, group!') + # ...to your contacts + await client.send_message('+34600123123', 'Hello, friend!') + # ...or even to any username + await client.send_message('username', 'Testing Telethon!') - # You can, of course, use markdown in your messages: - message = await client.send_message( - 'me', - 'This message has **bold**, `code`, __italics__ and ' - 'a [nice website](https://example.com)!', - link_preview=False - ) + # You can, of course, use markdown in your messages: + message = await client.send_message( + 'me', + 'This message has **bold**, `code`, __italics__ and ' + 'a [nice website](https://example.com)!', + link_preview=False + ) - # Sending a message returns the sent message object, which you can use - print(message.raw_text) + # Sending a message returns the sent message object, which you can use + print(message.raw_text) - # You can reply to messages directly if you have a message object - await message.reply('Cool!') + # You can reply to messages directly if you have a message object + await message.reply('Cool!') - # Or send files, songs, documents, albums... - await client.send_file('me', '/home/me/Pictures/holidays.jpg') + # Or send files, songs, documents, albums... + await client.send_file('me', '/home/me/Pictures/holidays.jpg') - # You can print the message history of any chat: - async for message in client.iter_messages('me'): - print(message.id, message.text) + # You can print the message history of any chat: + async for message in client.iter_messages('me'): + print(message.id, message.text) - # You can download media from messages, too! - # The method will return the path where the file was saved. - if message.photo: - path = await message.download_media() - print('File saved to', path) # printed after download is done + # You can download media from messages, too! + # The method will return the path where the file was saved. + if message.photo: + path = await message.download_media() + print('File saved to', path) # printed after download is done - with client: - client.loop.run_until_complete(main()) + asyncio.run(main()) Here, we show how to sign in, get information about yourself, send @@ -100,8 +100,8 @@ proceeding. We will see all the available methods later on. # Most of your code should go here. # You can of course make and use your own async def (do_something). # They only need to be async if they need to await things. - async with client: + async with client.start(): me = await client.get_me() await do_something(me) - client.loop.run_until_complete(main()) + asyncio.run(main()) diff --git a/readthedocs/basic/signing-in.rst b/readthedocs/basic/signing-in.rst index c7b60507..05183f5c 100644 --- a/readthedocs/basic/signing-in.rst +++ b/readthedocs/basic/signing-in.rst @@ -49,6 +49,7 @@ We can finally write some code to log into our account! .. code-block:: python + import asyncio from telethon import TelegramClient # Use your own values from my.telegram.org @@ -57,10 +58,10 @@ We can finally write some code to log into our account! async def main(): # The first parameter is the .session file name (absolute paths allowed) - async with TelegramClient('anon', api_id, api_hash) as client: + async with TelegramClient('anon', api_id, api_hash).start() as client: await client.send_message('me', 'Hello, myself!') - client.loop.run_until_complete(main()) + asyncio.run(main()) In the first line, we import the class name so we can create an instance @@ -98,21 +99,19 @@ You will still need an API ID and hash, but the process is very similar: .. code-block:: python + import asyncio from telethon import TelegramClient api_id = 12345 api_hash = '0123456789abcdef0123456789abcdef' bot_token = '12345:0123456789abcdef0123456789abcdef' - # We have to manually call "start" if we want an explicit bot token - bot = TelegramClient('bot', api_id, api_hash).start(bot_token=bot_token) - async def main(): # But then we can use the client instance as usual - async with bot: - ... + async with TelegramClient('bot', api_id, api_hash).start(bot_token=bot_token) as bot: + ... # bot is your client - client.loop.run_until_complete(main()) + asyncio.run(main()) To get a bot account, you need to talk diff --git a/readthedocs/concepts/full-api.rst b/readthedocs/concepts/full-api.rst index cce026f1..ac38e3d4 100644 --- a/readthedocs/concepts/full-api.rst +++ b/readthedocs/concepts/full-api.rst @@ -74,7 +74,7 @@ Or we call `client.get_input_entity() async def main(): peer = await client.get_input_entity('someone') - client.loop.run_until_complete(main()) + asyncio.run(main()) .. note:: diff --git a/readthedocs/concepts/sessions.rst b/readthedocs/concepts/sessions.rst index 8ba75938..028366b1 100644 --- a/readthedocs/concepts/sessions.rst +++ b/readthedocs/concepts/sessions.rst @@ -156,8 +156,8 @@ you can save it in a variable directly: .. code-block:: python string = '1aaNk8EX-YRfwoRsebUkugFvht6DUPi_Q25UOCzOAqzc...' - with TelegramClient(StringSession(string), api_id, api_hash) as client: - client.loop.run_until_complete(client.send_message('me', 'Hi')) + async with TelegramClient(StringSession(string), api_id, api_hash).start() as client: + await client.send_message('me', 'Hi') These strings are really convenient for using in places like Heroku since diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 10556cc0..33a515d8 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -759,3 +759,5 @@ they are meant to work on lists. also mark read only supports single now. a list would just be max anyway. removed max id since it's not really of much use. + +client loop has been removed. embrace implicit loop as asyncio does now diff --git a/readthedocs/modules/client.rst b/readthedocs/modules/client.rst index de5502c9..4fc076d8 100644 --- a/readthedocs/modules/client.rst +++ b/readthedocs/modules/client.rst @@ -20,10 +20,10 @@ Each mixin has its own methods, which you all can use. async def main(): # Now you can use all client methods listed below, like for example... - await client.send_message('me', 'Hello to myself!') + async with client.start(): + await client.send_message('me', 'Hello to myself!') - with client: - client.loop.run_until_complete(main()) + asyncio.run(main()) You **don't** need to import these `AuthMethods`, `MessageMethods`, etc. diff --git a/telethon/_client/chats.py b/telethon/_client/chats.py index f900ce6d..0759acc2 100644 --- a/telethon/_client/chats.py +++ b/telethon/_client/chats.py @@ -40,7 +40,7 @@ class _ChatAction: self._chat, self._action) self._running = True - self._task = self._client.loop.create_task(self._update()) + self._task = asyncio.create_task(self._update()) return self async def __aexit__(self, *args): diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 10cd2d7f..aeecedd6 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -88,7 +88,6 @@ def init( app_version: str = None, lang_code: str = 'en', system_lang_code: str = 'en', - loop: asyncio.AbstractEventLoop = None, base_logger: typing.Union[str, logging.Logger] = None, receive_updates: bool = True ): @@ -153,24 +152,6 @@ def init( self.api_id = int(api_id) self.api_hash = api_hash - # Current proxy implementation requires `sock_connect`, and some - # event loops lack this method. If the current loop is missing it, - # bail out early and suggest an alternative. - # - # TODO A better fix is obviously avoiding the use of `sock_connect` - # - # See https://github.com/LonamiWebs/Telethon/issues/1337 for details. - if not callable(getattr(self.loop, 'sock_connect', None)): - raise TypeError( - 'Event loop of type {} lacks `sock_connect`, which is needed to use proxies.\n\n' - 'Change the event loop in use to use proxies:\n' - '# https://github.com/LonamiWebs/Telethon/issues/1337\n' - 'import asyncio\n' - 'asyncio.set_event_loop(asyncio.SelectorEventLoop())'.format( - self.loop.__class__.__name__ - ) - ) - if local_addr is not None: if use_ipv6 is False and ':' in local_addr: raise TypeError( @@ -283,10 +264,6 @@ def init( # A place to store if channels are a megagroup or not (see `edit_admin`) self._megagroup_cache = {} - -def get_loop(self: 'TelegramClient') -> asyncio.AbstractEventLoop: - return asyncio.get_event_loop() - def get_flood_sleep_threshold(self): return self._flood_sleep_threshold @@ -382,7 +359,7 @@ async def connect(self: 'TelegramClient') -> None: await self.session.save() - self._updates_handle = self.loop.create_task(self._update_loop()) + self._updates_handle = asyncio.create_task(self._update_loop()) def is_connected(self: 'TelegramClient') -> bool: sender = getattr(self, '_sender', None) diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index d5a2fab7..c87f6e84 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -148,10 +148,6 @@ class TelegramClient: "System lang code" to be sent when creating the initial connection. Defaults to `lang_code`. - loop (`asyncio.AbstractEventLoop`, optional): - Asyncio event loop to use. Defaults to `asyncio.get_event_loop()`. - This argument is ignored. - base_logger (`str` | `logging.Logger`, optional): Base logger name or instance to use. If a `str` is given, it'll be passed to `logging.getLogger()`. If a @@ -2666,31 +2662,11 @@ class TelegramClient: app_version: str = None, lang_code: str = 'en', system_lang_code: str = 'en', - loop: asyncio.AbstractEventLoop = None, base_logger: typing.Union[str, logging.Logger] = None, receive_updates: bool = True ): telegrambaseclient.init(**locals()) - @property - def loop(self: 'TelegramClient') -> asyncio.AbstractEventLoop: - """ - Property with the ``asyncio`` event loop used by this client. - - Example - .. code-block:: python - - # Download media in the background - task = client.loop.create_task(message.download_media()) - - # Do some work - ... - - # Join the task (wait for it to complete) - await task - """ - return telegrambaseclient.get_loop(**locals()) - @property def flood_sleep_threshold(self): return telegrambaseclient.get_flood_sleep_threshold(**locals()) diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index 5f30fbd8..d1178304 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -159,14 +159,14 @@ def _process_update(self: 'TelegramClient', update, entities, others): channel_id = self._state_cache.get_channel_id(update) args = (update, entities, others, channel_id, self._state_cache[channel_id]) if self._dispatching_updates_queue is None: - task = self.loop.create_task(_dispatch_update(self, *args)) + task = asyncio.create_task(_dispatch_update(self, *args)) self._updates_queue.add(task) task.add_done_callback(lambda _: self._updates_queue.discard(task)) else: self._updates_queue.put_nowait(args) if not self._dispatching_updates_queue.is_set(): self._dispatching_updates_queue.set() - self.loop.create_task(_dispatch_queue_updates(self)) + asyncio.create_task(_dispatch_queue_updates(self)) self._state_cache.update(update) diff --git a/telethon/_events/album.py b/telethon/_events/album.py index 19ea4234..f290519d 100644 --- a/telethon/_events/album.py +++ b/telethon/_events/album.py @@ -37,15 +37,15 @@ class AlbumHack: # very short-lived but might as well try to do "the right thing". self._client = weakref.ref(client) self._event = event # parent event - self._due = client.loop.time() + _HACK_DELAY + self._due = asyncio.get_running_loop().time() + _HACK_DELAY - client.loop.create_task(self.deliver_event()) + asyncio.create_task(self.deliver_event()) def extend(self, messages): client = self._client() if client: # weakref may be dead self._event.messages.extend(messages) - self._due = client.loop.time() + _HACK_DELAY + self._due = asyncio.get_running_loop().time() + _HACK_DELAY async def deliver_event(self): while True: @@ -53,7 +53,7 @@ class AlbumHack: if client is None: return # weakref is dead, nothing to deliver - diff = self._due - client.loop.time() + diff = self._due - asyncio.get_running_loop().time() if diff <= 0: # We've hit our due time, deliver event. It won't respect # sequential updates but fixing that would just worsen this. diff --git a/telethon/_events/callbackquery.py b/telethon/_events/callbackquery.py index 15f8d12f..dd4f54aa 100644 --- a/telethon/_events/callbackquery.py +++ b/telethon/_events/callbackquery.py @@ -1,5 +1,7 @@ import re import struct +import asyncio +import functools from .common import EventBuilder, EventCommon, name_inner_event from .._misc import utils @@ -7,6 +9,20 @@ from .. import _tl from ..types import _custom +def auto_answer(func): + @functools.wraps(func) + async def wrapped(self, *args, **kwargs): + if self._answered: + return await func(*args, **kwargs) + else: + return (await asyncio.gather( + self._answer(), + func(*args, **kwargs), + ))[1] + + return wrapped + + @name_inner_event class CallbackQuery(EventBuilder): """ @@ -240,16 +256,15 @@ class CallbackQuery(EventBuilder): if self._answered: return + res = await self._client(_tl.fn.messages.SetBotCallbackAnswer( + query_id=self.query.query_id, + cache_time=cache_time, + alert=alert, + message=message, + url=url, + )) self._answered = True - return await self._client( - _tl.fn.messages.SetBotCallbackAnswer( - query_id=self.query.query_id, - cache_time=cache_time, - alert=alert, - message=message, - url=url - ) - ) + return res @property def via_inline(self): @@ -266,35 +281,36 @@ class CallbackQuery(EventBuilder): """ return isinstance(self.query, _tl.UpdateInlineBotCallbackQuery) + @auto_answer async def respond(self, *args, **kwargs): """ Responds to the message (not as a reply). Shorthand for `telethon.client.messages.MessageMethods.send_message` with ``entity`` already set. - This method also creates a task to `answer` the callback. + This method will also `answer` the callback if necessary. This method will likely fail if `via_inline` is `True`. """ - self._client.loop.create_task(self.answer()) return await self._client.send_message( await self.get_input_chat(), *args, **kwargs) + @auto_answer async def reply(self, *args, **kwargs): """ Replies to the message (as a reply). Shorthand for `telethon.client.messages.MessageMethods.send_message` with both ``entity`` and ``reply_to`` already set. - This method also creates a task to `answer` the callback. + This method will also `answer` the callback if necessary. This method will likely fail if `via_inline` is `True`. """ - self._client.loop.create_task(self.answer()) kwargs['reply_to'] = self.query.msg_id return await self._client.send_message( await self.get_input_chat(), *args, **kwargs) + @auto_answer async def edit(self, *args, **kwargs): """ Edits the message. Shorthand for @@ -303,7 +319,7 @@ class CallbackQuery(EventBuilder): Returns `True` if the edit was successful. - This method also creates a task to `answer` the callback. + This method will also `answer` the callback if necessary. .. note:: @@ -311,7 +327,6 @@ class CallbackQuery(EventBuilder): `Message.edit `, since the message object is normally not present. """ - self._client.loop.create_task(self.answer()) if isinstance(self.query.msg_id, _tl.InputBotInlineMessageID): return await self._client.edit_message( None, self.query.msg_id, *args, **kwargs @@ -322,6 +337,7 @@ class CallbackQuery(EventBuilder): *args, **kwargs ) + @auto_answer async def delete(self, *args, **kwargs): """ Deletes the message. Shorthand for @@ -332,11 +348,10 @@ class CallbackQuery(EventBuilder): this `delete` method. Use a `telethon.client.telegramclient.TelegramClient` instance directly. - This method also creates a task to `answer` the callback. + This method will also `answer` the callback if necessary. This method will likely fail if `via_inline` is `True`. """ - self._client.loop.create_task(self.answer()) return await self._client.delete_messages( await self.get_input_chat(), [self.query.msg_id], *args, **kwargs diff --git a/telethon/_events/inlinequery.py b/telethon/_events/inlinequery.py index d3cd6822..2c718e11 100644 --- a/telethon/_events/inlinequery.py +++ b/telethon/_events/inlinequery.py @@ -242,6 +242,6 @@ class InlineQuery(EventBuilder): if inspect.isawaitable(obj): return asyncio.ensure_future(obj) - f = asyncio.get_event_loop().create_future() + f = asyncio.get_running_loop().create_future() f.set_result(obj) return f diff --git a/telethon/_network/connection.py b/telethon/_network/connection.py index 26674aa2..e256b89a 100644 --- a/telethon/_network/connection.py +++ b/telethon/_network/connection.py @@ -23,13 +23,15 @@ class Connection: """ Establishes a connection with the server. """ - loop = asyncio.get_event_loop() + loop = asyncio.get_running_loop() sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setblocking(False) if self._local_addr: sock.bind(self._local_addr) + # TODO https://github.com/LonamiWebs/Telethon/issues/1337 may be an issue again + # perhaps we just need to ignore async connect on windows and block? await asyncio.wait_for(loop.sock_connect(sock, (self._ip, self._port)), timeout) self._sock = sock @@ -41,14 +43,14 @@ class Connection: if not self._sock: raise ConnectionError('not connected') - loop = asyncio.get_event_loop() + loop = asyncio.get_running_loop() await loop.sock_sendall(self._sock, self._transport.pack(data)) async def recv(self): if not self._sock: raise ConnectionError('not connected') - loop = asyncio.get_event_loop() + loop = asyncio.get_running_loop() while True: try: length, body = self._transport.unpack(self._in_buffer) diff --git a/telethon/_network/mtprotosender.py b/telethon/_network/mtprotosender.py index 29371bf6..42dd69a0 100644 --- a/telethon/_network/mtprotosender.py +++ b/telethon/_network/mtprotosender.py @@ -58,7 +58,7 @@ class MTProtoSender: # pending futures should be cancelled. self._user_connected = False self._reconnecting = False - self._disconnected = asyncio.get_event_loop().create_future() + self._disconnected = asyncio.get_running_loop().create_future() self._disconnected.set_result(None) # We need to join the loops upon disconnection @@ -248,18 +248,17 @@ class MTProtoSender: await self._disconnect(error=e) raise e - loop = asyncio.get_event_loop() self._log.debug('Starting send loop') - self._send_loop_handle = loop.create_task(self._send_loop()) + self._send_loop_handle = asyncio.create_task(self._send_loop()) self._log.debug('Starting receive loop') - self._recv_loop_handle = loop.create_task(self._recv_loop()) + self._recv_loop_handle = asyncio.create_task(self._recv_loop()) # _disconnected only completes after manual disconnection # or errors after which the sender cannot continue such # as failing to reconnect or any unexpected error. if self._disconnected.done(): - self._disconnected = loop.create_future() + self._disconnected = asyncio.get_running_loop().create_future() self._log.info('Connection to %s complete!', self._connection) @@ -381,7 +380,7 @@ class MTProtoSender: self._pending_state.clear() if self._auto_reconnect_callback: - asyncio.get_event_loop().create_task(self._auto_reconnect_callback()) + asyncio.create_task(self._auto_reconnect_callback()) break else: @@ -406,7 +405,7 @@ class MTProtoSender: # gets stuck. # TODO It still gets stuck? Investigate where and why. self._reconnecting = True - asyncio.get_event_loop().create_task(self._reconnect(error)) + asyncio.create_task(self._reconnect(error)) def _keepalive_ping(self, rnd_id): """ diff --git a/telethon_examples/gui.py b/telethon_examples/gui.py index bd241f60..61027d52 100644 --- a/telethon_examples/gui.py +++ b/telethon_examples/gui.py @@ -132,7 +132,7 @@ class App(tkinter.Tk): command=self.send_message).grid(row=3, column=2) # Post-init (async, connect client) - self.cl.loop.create_task(self.post_init()) + asyncio.create_task(self.post_init()) async def post_init(self): """ @@ -369,10 +369,4 @@ async def main(interval=0.05): if __name__ == "__main__": - # Some boilerplate code to set up the main method - aio_loop = asyncio.get_event_loop() - try: - aio_loop.run_until_complete(main()) - finally: - if not aio_loop.is_closed(): - aio_loop.close() + asyncio.run(main()) diff --git a/telethon_examples/interactive_telegram_client.py b/telethon_examples/interactive_telegram_client.py index 88f491de..10ca71a1 100644 --- a/telethon_examples/interactive_telegram_client.py +++ b/telethon_examples/interactive_telegram_client.py @@ -9,9 +9,6 @@ from telethon.errors import SessionPasswordNeededError from telethon.network import ConnectionTcpAbridged from telethon.utils import get_display_name -# Create a global variable to hold the loop we will be using -loop = asyncio.get_event_loop() - def sprint(string, *args, **kwargs): """Safe Print (handle UnicodeEncodeErrors on some terminals)""" @@ -50,7 +47,7 @@ async def async_input(prompt): let the loop run while we wait for input. """ print(prompt, end='', flush=True) - return (await loop.run_in_executor(None, sys.stdin.readline)).rstrip() + return (await asyncio.get_running_loop().run_in_executor(None, sys.stdin.readline)).rstrip() def get_env(name, message, cast=str): @@ -109,34 +106,34 @@ class InteractiveTelegramClient(TelegramClient): # media known the message ID, for every message having media. self.found_media = {} + async def init(self): # Calling .connect() may raise a connection error False, so you need # to except those before continuing. Otherwise you may want to retry # as done here. print('Connecting to Telegram servers...') try: - loop.run_until_complete(self.connect()) + await self.connect() except IOError: # We handle IOError and not ConnectionError because # PySocks' errors do not subclass ConnectionError # (so this will work with and without proxies). print('Initial connection failed. Retrying...') - loop.run_until_complete(self.connect()) + await self.connect() # If the user hasn't called .sign_in() or .sign_up() yet, they won't # be authorized. The first thing you must do is authorize. Calling # .sign_in() should only be done once as the information is saved on # the *.session file so you don't need to enter the code every time. - if not loop.run_until_complete(self.is_user_authorized()): + if not await self.is_user_authorized(): print('First run. Sending code request...') user_phone = input('Enter your phone: ') - loop.run_until_complete(self.sign_in(user_phone)) + await self.sign_in(user_phone) self_user = None while self_user is None: code = input('Enter the code you just received: ') try: - self_user =\ - loop.run_until_complete(self.sign_in(code=code)) + self_user = await self.sign_in(code=code) # Two-step verification may be enabled, and .sign_in will # raise this error. If that's the case ask for the password. @@ -146,8 +143,7 @@ class InteractiveTelegramClient(TelegramClient): pw = getpass('Two step verification is enabled. ' 'Please enter your password: ') - self_user =\ - loop.run_until_complete(self.sign_in(password=pw)) + self_user = await self.sign_in(password=pw) async def run(self): """Main loop of the TelegramClient, will wait for user action""" @@ -397,9 +393,13 @@ class InteractiveTelegramClient(TelegramClient): )) -if __name__ == '__main__': +async def main(): SESSION = os.environ.get('TG_SESSION', 'interactive') API_ID = get_env('TG_API_ID', 'Enter your API ID: ', int) API_HASH = get_env('TG_API_HASH', 'Enter your API hash: ') - client = InteractiveTelegramClient(SESSION, API_ID, API_HASH) - loop.run_until_complete(client.run()) + client = await InteractiveTelegramClient(SESSION, API_ID, API_HASH).init() + await client.run() + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/telethon_examples/payment.py b/telethon_examples/payment.py index 0e9f9733..3eab88f3 100644 --- a/telethon_examples/payment.py +++ b/telethon_examples/payment.py @@ -7,8 +7,6 @@ import os import time import sys -loop = asyncio.get_event_loop() - """ Provider token can be obtained via @BotFather. more info at https://core.telegram.org/bots/payments#getting-a-token @@ -180,4 +178,4 @@ if __name__ == '__main__': if not provider_token: logger.error("No provider token supplied.") exit(1) - loop.run_until_complete(main()) + asyncio.run(main()) diff --git a/telethon_examples/quart_login.py b/telethon_examples/quart_login.py index 98fb35de..20eae383 100644 --- a/telethon_examples/quart_login.py +++ b/telethon_examples/quart_login.py @@ -134,12 +134,13 @@ async def main(): # By default, `Quart.run` uses `asyncio.run()`, which creates a new asyncio -# event loop. If we create the `TelegramClient` before, `telethon` will -# use `asyncio.get_event_loop()`, which is the implicit loop in the main -# thread. These two loops are different, and it won't work. +# event loop. Instead, we use `asyncio.run()` manually in order to make this +# explicit, as the client cannot be "transferred" between loops while +# connected due to the need to schedule work within an event loop. # -# So, we have to manually pass the same `loop` to both applications to -# make 100% sure it works and to avoid headaches. +# In essence one needs to be careful to avoid mixing event loops, but this is +# simple, as `asyncio.run` is generally only used in the entry-point of the +# program. # # To run Quart inside `async def`, we must use `hypercorn.asyncio.serve()` # directly. @@ -149,4 +150,4 @@ async def main(): # won't have to worry about any of this, but it's still good to be # explicit about the event loop. if __name__ == '__main__': - client.loop.run_until_complete(main()) + asyncio.run(main()) From 4f4c7040d17e51c9b925e460376e8ad0088cbb99 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 16 Jan 2022 13:59:43 +0100 Subject: [PATCH 142/256] Stop using futures as one-shot channels Instead, use a single-item queue. This is asyncio.run-friendly, even when the client is initialized outside of async def. --- telethon/_client/updates.py | 2 +- telethon/_network/mtprotosender.py | 29 ++++++++++++----------------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index d1178304..a0041310 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -30,7 +30,7 @@ async def set_receive_updates(self: 'TelegramClient', receive_updates): async def run_until_disconnected(self: 'TelegramClient'): # Make a high-level request to notify that we want updates await self(_tl.fn.updates.GetState()) - return await self._sender.disconnected + await self._sender.wait_disconnected() def on(self: 'TelegramClient', event: EventBuilder): def decorator(f): diff --git a/telethon/_network/mtprotosender.py b/telethon/_network/mtprotosender.py index 42dd69a0..92438502 100644 --- a/telethon/_network/mtprotosender.py +++ b/telethon/_network/mtprotosender.py @@ -58,8 +58,8 @@ class MTProtoSender: # pending futures should be cancelled. self._user_connected = False self._reconnecting = False - self._disconnected = asyncio.get_running_loop().create_future() - self._disconnected.set_result(None) + self._disconnected = asyncio.Queue(1) + self._disconnected.put_nowait(None) # We need to join the loops upon disconnection self._send_loop_handle = None @@ -191,16 +191,14 @@ class MTProtoSender: self._send_queue.extend(states) return futures - @property - def disconnected(self): + async def wait_disconnected(self): """ - Future that resolves when the connection to Telegram - ends, either by user action or in the background. - - Note that it may resolve in either a ``ConnectionError`` - or any other unexpected error that could not be handled. + Wait until the client is disconnected. + Raise if the disconnection finished with error. """ - return asyncio.shield(self._disconnected) + res = await self._disconnected.get() + if isinstance(res, BaseException): + raise res # Private methods @@ -257,8 +255,8 @@ class MTProtoSender: # _disconnected only completes after manual disconnection # or errors after which the sender cannot continue such # as failing to reconnect or any unexpected error. - if self._disconnected.done(): - self._disconnected = asyncio.get_running_loop().create_future() + while not self._disconnected.empty(): + self._disconnected.get_nowait() self._log.info('Connection to %s complete!', self._connection) @@ -316,11 +314,8 @@ class MTProtoSender: self._log.info('Disconnection from %s complete!', self._connection) self._connection = None - if self._disconnected and not self._disconnected.done(): - if error: - self._disconnected.set_exception(error) - else: - self._disconnected.set_result(None) + if not self._disconnected.full(): + self._disconnected.put_nowait(error) async def _reconnect(self, last_error): """ From 1f1f67b0a666b4fbb1e49f9e1acd7e5da7cd8d6e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 16 Jan 2022 14:04:26 +0100 Subject: [PATCH 143/256] Remove unused _CacheType --- telethon/_client/uploads.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/telethon/_client/uploads.py b/telethon/_client/uploads.py index 5cfd43e4..92a2d36e 100644 --- a/telethon/_client/uploads.py +++ b/telethon/_client/uploads.py @@ -24,18 +24,6 @@ if typing.TYPE_CHECKING: from .telegramclient import TelegramClient -class _CacheType: - """Like functools.partial but pretends to be the wrapped class.""" - def __init__(self, cls): - self._cls = cls - - def __call__(self, *args, **kwargs): - return self._cls(*args, file_reference=b'', **kwargs) - - def __eq__(self, other): - return self._cls == other - - def _resize_photo_if_needed( file, is_image, width=1280, height=1280, background=(255, 255, 255)): From 3f68510393112699aea7c7c46b608cbbcfe33701 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 17 Jan 2022 11:41:08 +0100 Subject: [PATCH 144/256] Fix EntityCache not reading the new EntityType --- telethon/_misc/entitycache.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/telethon/_misc/entitycache.py b/telethon/_misc/entitycache.py index 87bde9fa..2b7b8af1 100644 --- a/telethon/_misc/entitycache.py +++ b/telethon/_misc/entitycache.py @@ -3,7 +3,7 @@ import itertools from .._misc import utils from .. import _tl -from .._sessions.types import Entity +from .._sessions.types import EntityType, Entity # Which updates have the following fields? _has_field = { @@ -53,18 +53,18 @@ class EntityCache: In-memory input entity cache, defaultdict-like behaviour. """ def add(self, entities, _mappings={ - _tl.User.CONSTRUCTOR_ID: lambda e: (Entity.BOT if e.bot else Entity.USER, e.id, e.access_hash), - _tl.UserFull.CONSTRUCTOR_ID: lambda e: (Entity.BOT if e.user.bot else Entity.USER, e.user.id, e.user.access_hash), - _tl.Chat.CONSTRUCTOR_ID: lambda e: (Entity.GROUP, e.id, 0), - _tl.ChatFull.CONSTRUCTOR_ID: lambda e: (Entity.GROUP, e.id, 0), - _tl.ChatEmpty.CONSTRUCTOR_ID: lambda e: (Entity.GROUP, e.id, 0), - _tl.ChatForbidden.CONSTRUCTOR_ID: lambda e: (Entity.GROUP, e.id, 0), + _tl.User.CONSTRUCTOR_ID: lambda e: (EntityType.BOT if e.bot else EntityType.USER, e.id, e.access_hash), + _tl.UserFull.CONSTRUCTOR_ID: lambda e: (EntityType.BOT if e.user.bot else EntityType.USER, e.user.id, e.user.access_hash), + _tl.Chat.CONSTRUCTOR_ID: lambda e: (EntityType.GROUP, e.id, 0), + _tl.ChatFull.CONSTRUCTOR_ID: lambda e: (EntityType.GROUP, e.id, 0), + _tl.ChatEmpty.CONSTRUCTOR_ID: lambda e: (EntityType.GROUP, e.id, 0), + _tl.ChatForbidden.CONSTRUCTOR_ID: lambda e: (EntityType.GROUP, e.id, 0), _tl.Channel.CONSTRUCTOR_ID: lambda e: ( - Entity.MEGAGROUP if e.megagroup else (Entity.GIGAGROUP if e.gigagroup else Entity.CHANNEL), + EntityType.MEGAGROUP if e.megagroup else (EntityType.GIGAGROUP if e.gigagroup else EntityType.CHANNEL), e.id, e.access_hash, ), - _tl.ChannelForbidden.CONSTRUCTOR_ID: lambda e: (Entity.MEGAGROUP if e.megagroup else Entity.CHANNEL, e.id, e.access_hash), + _tl.ChannelForbidden.CONSTRUCTOR_ID: lambda e: (EntityType.MEGAGROUP if e.megagroup else EntityType.CHANNEL, e.id, e.access_hash), }): """ Adds the given entities to the cache, if they weren't saved before. @@ -98,11 +98,11 @@ class EntityCache: if not getattr(e, 'min', False) and (access_hash or ty == Entity.GROUP): rows.append(Entity(ty, id, access_hash)) if id not in self.__dict__: - if ty in (Entity.USER, Entity.BOT): + if ty in (EntityType.USER, EntityType.BOT): self.__dict__[id] = _tl.InputPeerUser(id, access_hash) - elif ty in (Entity.GROUP,): + elif ty in (EntityType.GROUP,): self.__dict__[id] = _tl.InputPeerChat(id) - elif ty in (Entity.CHANNEL, Entity.MEGAGROUP, Entity.GIGAGROUP): + elif ty in (EntityType.CHANNEL, EntityType.MEGAGROUP, EntityType.GIGAGROUP): self.__dict__[id] = _tl.InputPeerChannel(id, access_hash) return rows From 85a9c13129e45edd72a6de70b542694233b2423f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 17 Jan 2022 11:50:28 +0100 Subject: [PATCH 145/256] Fix login info did not persist --- telethon/_client/telegrambaseclient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index aeecedd6..bdf8d539 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -320,7 +320,7 @@ async def connect(self: 'TelegramClient') -> None: return if self._sender.auth_key.key != dc.auth: - dc = dataclasses.replace(dc, auth=self._sender.auth_key.key) + self._all_dcs[dc.id] = dc = dataclasses.replace(dc, auth=self._sender.auth_key.key) # Need to send invokeWithLayer for things to work out. # Make the most out of this opportunity by also refreshing our state. From f8264abb5a14926b52c21dfd6803c9f5e2a882b6 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 18 Jan 2022 12:52:22 +0100 Subject: [PATCH 146/256] Clean-up client's __init__ and remove entity cache Entity cache uses are removed. It was a source of ever-growing memory usage that has to be reworked. This affects everything that tried to obtain an input entity, input sender or input chat (such as the SenderGetter or calls to _get_entity_pair). Input entities need to be reworked in any case. Its removal also affects the automatic cache of any raw API request. Raise last error parameter is removed, and its behaviour made default. The connection type parameter has been removed, since users really have no need to change it. A few more attributes have been made private, since users should not mess with those. --- readthedocs/misc/v2-migration-guide.rst | 10 ++ telethon/_client/auth.py | 11 +- telethon/_client/telegrambaseclient.py | 161 ++++++++---------------- telethon/_client/telegramclient.py | 23 ++-- telethon/_client/updates.py | 29 +---- telethon/_client/users.py | 36 ++---- telethon/_events/album.py | 3 +- telethon/_events/callbackquery.py | 14 +-- telethon/_events/chataction.py | 9 +- telethon/_events/common.py | 3 +- telethon/_events/inlinequery.py | 3 +- telethon/_events/userupdate.py | 3 +- telethon/_misc/utils.py | 12 +- telethon/types/_custom/chatgetter.py | 6 - telethon/types/_custom/draft.py | 6 - telethon/types/_custom/forward.py | 6 +- telethon/types/_custom/message.py | 16 +-- telethon/types/_custom/sendergetter.py | 6 - 18 files changed, 104 insertions(+), 253 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 33a515d8..669ed8a8 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -761,3 +761,13 @@ also mark read only supports single now. a list would just be max anyway. removed max id since it's not really of much use. client loop has been removed. embrace implicit loop as asyncio does now + +renamed some client params, and made other privates + timeout -> connect_timeout + connection_retries -> connect_retries + retry_delay -> connect_retry_delay + +sequential_updates is gone +connection type is gone + +raise_last_call_error is now the default rather than ValueError diff --git a/telethon/_client/auth.py b/telethon/_client/auth.py index 90bb5ae5..69b5df7e 100644 --- a/telethon/_client/auth.py +++ b/telethon/_client/auth.py @@ -241,7 +241,7 @@ async def sign_in( elif bot_token: request = _tl.fn.auth.ImportBotAuthorization( flags=0, bot_auth_token=bot_token, - api_id=self.api_id, api_hash=self.api_hash + api_id=self._api_id, api_hash=self._api_hash ) else: raise ValueError('You must provide either phone and code, password, or bot_token.') @@ -313,8 +313,6 @@ async def _update_session_state(self, user, save=True): Callback called whenever the login or sign up process completes. Returns the input user parameter. """ - self._authorized = True - state = await self(_tl.fn.updates.GetState()) await _replace_session_state( self, @@ -332,11 +330,11 @@ async def _update_session_state(self, user, save=True): async def _replace_session_state(self, *, save=True, **changes): new = dataclasses.replace(self._session_state, **changes) - await self.session.set_state(new) + await self._session.set_state(new) self._session_state = new if save: - await self.session.save() + await self._session.save() async def send_code_request( @@ -354,7 +352,7 @@ async def send_code_request( else: try: result = await self(_tl.fn.auth.SendCode( - phone, self.api_id, self.api_hash, _tl.CodeSettings())) + phone, self._api_id, self._api_hash, _tl.CodeSettings())) except errors.AuthRestartError: return await self.send_code_request(phone) @@ -377,7 +375,6 @@ async def log_out(self: 'TelegramClient') -> bool: except errors.RPCError: return False - self._authorized = False self._state_cache.reset() await self.disconnect() diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index bdf8d539..9ed5cd71 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -11,7 +11,7 @@ import dataclasses from .. import version, __name__ as __base_name__, _tl from .._crypto import rsa -from .._misc import markdown, entitycache, statecache, enums, helpers +from .._misc import markdown, statecache, enums, helpers from .._network import MTProtoSender, Connection, transports from .._sessions import Session, SQLiteSession, MemorySession from .._sessions.types import DataCenter, SessionState @@ -71,33 +71,28 @@ def init( api_id: int, api_hash: str, *, - connection: 'typing.Type[Connection]' = (), + # Logging. + base_logger: typing.Union[str, logging.Logger] = None, + # Connection parameters. use_ipv6: bool = False, proxy: typing.Union[tuple, dict] = None, local_addr: typing.Union[str, tuple] = None, - timeout: int = 10, - request_retries: int = 5, - connection_retries: int = 5, - retry_delay: int = 1, - auto_reconnect: bool = True, - sequential_updates: bool = False, - flood_sleep_threshold: int = 60, - raise_last_call_error: bool = False, device_model: str = None, system_version: str = None, app_version: str = None, lang_code: str = 'en', system_lang_code: str = 'en', - base_logger: typing.Union[str, logging.Logger] = None, - receive_updates: bool = True + # Nice-to-have. + auto_reconnect: bool = True, + connect_timeout: int = 10, + connect_retries: int = 4, + connect_retry_delay: int = 1, + request_retries: int = 4, + flood_sleep_threshold: int = 60, + # Update handling. + receive_updates: bool = True, ): - if not api_id or not api_hash: - raise ValueError( - "Your API ID or Hash cannot be empty or None. " - "Refer to telethon.rtfd.io for more information.") - - self._use_ipv6 = use_ipv6 - + # Logging. if isinstance(base_logger, str): base_logger = logging.getLogger(base_logger) elif not isinstance(base_logger, logging.Logger): @@ -112,7 +107,7 @@ def init( self._log = _Loggers() - # Determine what session object we have + # Sessions. if isinstance(session, str) or session is None: try: session = SQLiteSession(session) @@ -131,57 +126,38 @@ def init( 'The given session must be a str or a Session instance.' ) - self.flood_sleep_threshold = flood_sleep_threshold - - # TODO Use AsyncClassWrapper(session) - # ChatGetter and SenderGetter can use the in-memory _entity_cache - # to avoid network access and the need for await in session files. - # - # The session files only wants the entities to persist - # them to disk, and to save additional useful information. - # TODO Session should probably return all cached - # info of entities, not just the input versions - self.session = session - - # Cache session data for convenient access + self._session = session + # In-memory copy of the session's state to avoid a roundtrip as it contains commonly-accessed values. self._session_state = None - self._all_dcs = None - self._state_cache = statecache.StateCache(None, self._log) - self._entity_cache = entitycache.EntityCache() - self.api_id = int(api_id) - self.api_hash = api_hash + # Nice-to-have. + self._request_retries = request_retries + self._connect_retries = connect_retries + self._connect_retry_delay = connect_retry_delay or 0 + self._connect_timeout = connect_timeout + self.flood_sleep_threshold = flood_sleep_threshold + self._flood_waited_requests = {} # prevent calls that would floodwait entirely + self._parse_mode = markdown + + # Connection parameters. + if not api_id or not api_hash: + raise ValueError( + "Your API ID or Hash cannot be empty or None. " + "Refer to telethon.rtfd.io for more information.") if local_addr is not None: if use_ipv6 is False and ':' in local_addr: - raise TypeError( - 'A local IPv6 address must only be used with `use_ipv6=True`.' - ) + raise TypeError('A local IPv6 address must only be used with `use_ipv6=True`.') elif use_ipv6 is True and ':' not in local_addr: - raise TypeError( - '`use_ipv6=True` must only be used with a local IPv6 address.' - ) + raise TypeError('`use_ipv6=True` must only be used with a local IPv6 address.') - self._raise_last_call_error = raise_last_call_error - - self._request_retries = request_retries - self._connection_retries = connection_retries - self._retry_delay = retry_delay or 0 - self._proxy = proxy + self._transport = transports.Full() + self._use_ipv6 = use_ipv6 self._local_addr = local_addr - self._timeout = timeout + self._proxy = proxy self._auto_reconnect = auto_reconnect - - if connection == (): - # For now the current default remains TCP Full; may change to be "smart" if proxies are specified - connection = enums.ConnectionMode.FULL - - self._transport = { - enums.ConnectionMode.FULL: transports.Full(), - enums.ConnectionMode.INTERMEDIATE: transports.Intermediate(), - enums.ConnectionMode.ABRIDGED: transports.Abridged(), - }[enums.ConnectionMode(connection)] - init_proxy = None + self._api_id = int(api_id) + self._api_hash = api_hash # Used on connection. Capture the variables in a lambda since # exporting clients need to create this InvokeWithLayer. @@ -196,7 +172,7 @@ def init( default_system_version = re.sub(r'-.+','',system.release) self._init_request = _tl.fn.InitConnection( - api_id=self.api_id, + api_id=self._api_id, device_model=device_model or default_device_model or 'Unknown', system_version=system_version or default_system_version or '1.0', app_version=app_version or self.__version__, @@ -204,65 +180,26 @@ def init( system_lang_code=system_lang_code, lang_pack='', # "langPacks are for official apps only" query=None, - proxy=init_proxy + proxy=None ) self._sender = MTProtoSender( loggers=self._log, - retries=self._connection_retries, - delay=self._retry_delay, + retries=self._connect_retries, + delay=self._connect_retry_delay, auto_reconnect=self._auto_reconnect, - connect_timeout=self._timeout, + connect_timeout=self._connect_timeout, update_callback=self._handle_update, auto_reconnect_callback=self._handle_auto_reconnect ) - # Remember flood-waited requests to avoid making them again - self._flood_waited_requests = {} - - # Cache ``{dc_id: (_ExportState, MTProtoSender)}`` for all borrowed senders + # Cache ``{dc_id: (_ExportState, MTProtoSender)}`` for all borrowed senders. self._borrowed_senders = {} self._borrow_sender_lock = asyncio.Lock() - self._updates_handle = None - self._last_request = time.time() - self._channel_pts = {} + # Update handling. self._no_updates = not receive_updates - if sequential_updates: - self._updates_queue = asyncio.Queue() - self._dispatching_updates_queue = asyncio.Event() - else: - # Use a set of pending instead of a queue so we can properly - # terminate all pending updates on disconnect. - self._updates_queue = set() - self._dispatching_updates_queue = None - - self._authorized = None # None = unknown, False = no, True = yes - - # Some further state for subclasses - self._event_builders = [] - - # Hack to workaround the fact Telegram may send album updates as - # different Updates when being sent from a different data center. - # {grouped_id: AlbumHack} - # - # FIXME: We don't bother cleaning this up because it's not really - # worth it, albums are pretty rare and this only holds them - # for a second at most. - self._albums = {} - - # Default parse mode - self._parse_mode = markdown - - # Some fields to easy signing in. Let {phone: hash} be - # a dictionary because the user may change their mind. - self._phone_code_hash = {} - self._phone = None - self._tos = None - - # A place to store if channels are a megagroup or not (see `edit_admin`) - self._megagroup_cache = {} def get_flood_sleep_threshold(self): return self._flood_sleep_threshold @@ -273,8 +210,8 @@ def set_flood_sleep_threshold(self, value): async def connect(self: 'TelegramClient') -> None: - self._all_dcs = {dc.id: dc for dc in await self.session.get_all_dc()} - self._session_state = await self.session.get_state() + self._all_dcs = {dc.id: dc for dc in await self._session.get_all_dc()} + self._session_state = await self._session.get_state() if self._session_state is None: try_fetch_user = False @@ -347,7 +284,7 @@ async def connect(self: 'TelegramClient') -> None: self._all_dcs[dc.id] = DataCenter(dc.id, ip, None, dc.port, b'') for dc in self._all_dcs.values(): - await self.session.insert_dc(dc) + await self._session.insert_dc(dc) if try_fetch_user: # If there was a previous session state, but the current user ID is 0, it means we've @@ -357,7 +294,7 @@ async def connect(self: 'TelegramClient') -> None: if me: await self._update_session_state(me, save=False) - await self.session.save() + await self._session.save() self._updates_handle = asyncio.create_task(self._update_loop()) diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index c87f6e84..5f555b06 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -2645,25 +2645,26 @@ class TelegramClient: api_id: int, api_hash: str, *, - connection: typing.Union[str, enums.ConnectionMode] = (), + # Logging. + base_logger: typing.Union[str, logging.Logger] = None, + # Connection parameters. use_ipv6: bool = False, proxy: typing.Union[tuple, dict] = None, local_addr: typing.Union[str, tuple] = None, - timeout: int = 10, - request_retries: int = 5, - connection_retries: int = 5, - retry_delay: int = 1, - auto_reconnect: bool = True, - sequential_updates: bool = False, - flood_sleep_threshold: int = 60, - raise_last_call_error: bool = False, device_model: str = None, system_version: str = None, app_version: str = None, lang_code: str = 'en', system_lang_code: str = 'en', - base_logger: typing.Union[str, logging.Logger] = None, - receive_updates: bool = True + # Nice-to-have. + auto_reconnect: bool = True, + connect_timeout: int = 10, + connect_retries: int = 4, + connect_retry_delay: int = 1, + request_retries: int = 4, + flood_sleep_threshold: int = 60, + # Update handling. + receive_updates: bool = True, ): telegrambaseclient.init(**locals()) diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index a0041310..141f6b95 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -206,7 +206,7 @@ async def _update_loop(self: 'TelegramClient'): # Entities are not saved when they are inserted because this is a rather expensive # operation (default's sqlite3 takes ~0.1s to commit changes). Do it every minute # instead. No-op if there's nothing new. - await self.session.save() + await self._session.save() # We need to send some content-related request at least hourly # for Telegram to keep delivering updates, otherwise they will @@ -231,33 +231,6 @@ async def _dispatch_queue_updates(self: 'TelegramClient'): self._dispatching_updates_queue.clear() async def _dispatch_update(self: 'TelegramClient', update, entities, others, channel_id, pts_date): - if entities: - rows = self._entity_cache.add(list(entities.values())) - if rows: - await self.session.insert_entities(rows) - - if not self._entity_cache.ensure_cached(update): - # We could add a lock to not fetch the same pts twice if we are - # already fetching it. However this does not happen in practice, - # which makes sense, because different updates have different pts. - if self._state_cache.update(update, check_only=True): - # If the update doesn't have pts, fetching won't do anything. - # For example, UpdateUserStatus or UpdateChatUserTyping. - try: - await _get_difference(self, update, entities, channel_id, pts_date) - except OSError: - pass # We were disconnected, that's okay - except RpcError: - # There's a high chance the request fails because we lack - # the channel. Because these "happen sporadically" (#1428) - # we should be okay (no flood waits) even if more occur. - pass - except ValueError: - # There is a chance that GetFullChannel and GetDifference - # inside the _get_difference() function will end up with - # ValueError("Request was unsuccessful N time(s)") for whatever reasons. - pass - built = EventBuilderDict(self, update, entities, others) for builder, callback in self._event_builders: diff --git a/telethon/_client/users.py b/telethon/_client/users.py index 69f2c564..9f381af6 100644 --- a/telethon/_client/users.py +++ b/telethon/_client/users.py @@ -76,9 +76,6 @@ async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sl exceptions.append(e) results.append(None) continue - entities = self._entity_cache.add(result) - if entities: - await self.session.insert_entities(entities) exceptions.append(None) results.append(result) request_index += 1 @@ -88,9 +85,6 @@ async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sl return results else: result = await future - entities = self._entity_cache.add(result) - if entities: - await self.session.insert_entities(entities) return result except ServerError as e: last_error = e @@ -129,10 +123,7 @@ async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sl raise await self._switch_dc(e.new_dc) - if self._raise_last_call_error and last_error is not None: - raise last_error - raise ValueError('Request was unsuccessful {} time(s)' - .format(attempt)) + raise last_error async def get_me(self: 'TelegramClient', input_peer: bool = False) \ @@ -147,15 +138,12 @@ async def is_bot(self: 'TelegramClient') -> bool: return self._session_state.bot if self._session_state else False async def is_user_authorized(self: 'TelegramClient') -> bool: - if self._authorized is None: - try: - # Any request that requires authorization will work - await self(_tl.fn.updates.GetState()) - self._authorized = True - except RpcError: - self._authorized = False - - return self._authorized + try: + # Any request that requires authorization will work + await self(_tl.fn.updates.GetState()) + return True + except RpcError: + return False async def get_entity( self: 'TelegramClient', @@ -236,14 +224,6 @@ async def get_input_entity( except TypeError: pass - # Next in priority is having a peer (or its ID) cached in-memory - try: - # 0x2d45687 == crc32(b'Peer') - if isinstance(peer, int) or peer.SUBCLASS_OF_ID == 0x2d45687: - return self._entity_cache[peer] - except (AttributeError, KeyError): - pass - # Then come known strings that take precedence if peer in ('me', 'self'): return _tl.InputPeerSelf() @@ -254,7 +234,7 @@ async def get_input_entity( except TypeError: pass else: - entity = await self.session.get_entity(None, peer_id) + entity = await self._session.get_entity(None, peer_id) if entity: if entity.ty in (Entity.USER, Entity.BOT): return _tl.InputPeerUser(entity.id, entity.access_hash) diff --git a/telethon/_events/album.py b/telethon/_events/album.py index f290519d..580e5a31 100644 --- a/telethon/_events/album.py +++ b/telethon/_events/album.py @@ -165,8 +165,7 @@ class Album(EventBuilder): def _set_client(self, client): super()._set_client(client) - self._sender, self._input_sender = utils._get_entity_pair( - self.sender_id, self._entities, client._entity_cache) + self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities) self.messages = [ _custom.Message._new(client, m, self._entities, None) diff --git a/telethon/_events/callbackquery.py b/telethon/_events/callbackquery.py index dd4f54aa..0e3e2d67 100644 --- a/telethon/_events/callbackquery.py +++ b/telethon/_events/callbackquery.py @@ -166,8 +166,7 @@ class CallbackQuery(EventBuilder): def _set_client(self, client): super()._set_client(client) - self._sender, self._input_sender = utils._get_entity_pair( - self.sender_id, self._entities, client._entity_cache) + self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities) @property def id(self): @@ -223,13 +222,10 @@ class CallbackQuery(EventBuilder): self._input_sender = utils.get_input_peer(self._chat) if not getattr(self._input_sender, 'access_hash', True): # getattr with True to handle the InputPeerSelf() case - try: - self._input_sender = self._client._entity_cache[self._sender_id] - except KeyError: - m = await self.get_message() - if m: - self._sender = m._sender - self._input_sender = m._input_sender + m = await self.get_message() + if m: + self._sender = m._sender + self._input_sender = m._input_sender async def answer( self, message=None, cache_time=0, *, url=None, alert=False): diff --git a/telethon/_events/chataction.py b/telethon/_events/chataction.py index b34ecd65..671347c0 100644 --- a/telethon/_events/chataction.py +++ b/telethon/_events/chataction.py @@ -401,20 +401,13 @@ class ChatAction(EventBuilder): if self._input_users is None and self._user_ids: self._input_users = [] for user_id in self._user_ids: - # First try to get it from our entities + # Try to get it from our entities try: self._input_users.append(utils.get_input_peer(self._entities[user_id])) continue except (KeyError, TypeError): pass - # If missing, try from the entity cache - try: - self._input_users.append(self._client._entity_cache[user_id]) - continue - except KeyError: - pass - return self._input_users or [] async def get_input_users(self): diff --git a/telethon/_events/common.py b/telethon/_events/common.py index bfaf3227..fb941980 100644 --- a/telethon/_events/common.py +++ b/telethon/_events/common.py @@ -147,8 +147,7 @@ class EventCommon(ChatGetter, abc.ABC): # TODO Nuke self._client = client if self._chat_peer: - self._chat, self._input_chat = utils._get_entity_pair( - self.chat_id, self._entities, client._entity_cache) + self._chat, self._input_chat = utils._get_entity_pair(self.chat_id, self._entities) else: self._chat = self._input_chat = None diff --git a/telethon/_events/inlinequery.py b/telethon/_events/inlinequery.py index 2c718e11..76401962 100644 --- a/telethon/_events/inlinequery.py +++ b/telethon/_events/inlinequery.py @@ -98,8 +98,7 @@ class InlineQuery(EventBuilder): def _set_client(self, client): super()._set_client(client) - self._sender, self._input_sender = utils._get_entity_pair( - self.sender_id, self._entities, client._entity_cache) + self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities) @property def id(self): diff --git a/telethon/_events/userupdate.py b/telethon/_events/userupdate.py index b5354ae0..35e8044c 100644 --- a/telethon/_events/userupdate.py +++ b/telethon/_events/userupdate.py @@ -94,8 +94,7 @@ class UserUpdate(EventBuilder): def _set_client(self, client): super()._set_client(client) - self._sender, self._input_sender = utils._get_entity_pair( - self.sender_id, self._entities, client._entity_cache) + self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities) @property def user(self): diff --git a/telethon/_misc/utils.py b/telethon/_misc/utils.py index e412d563..ca2b1388 100644 --- a/telethon/_misc/utils.py +++ b/telethon/_misc/utils.py @@ -578,20 +578,16 @@ def get_input_group_call(call): _raise_cast_fail(call, 'InputGroupCall') -def _get_entity_pair(entity_id, entities, cache, +def _get_entity_pair(entity_id, entities, get_input_peer=get_input_peer): """ Returns ``(entity, input_entity)`` for the given entity ID. """ entity = entities.get(entity_id) try: - input_entity = cache[entity_id] - except KeyError: - # KeyError is unlikely, so another TypeError won't hurt - try: - input_entity = get_input_peer(entity) - except TypeError: - input_entity = None + input_entity = get_input_peer(entity) + except TypeError: + input_entity = None return entity, input_entity diff --git a/telethon/types/_custom/chatgetter.py b/telethon/types/_custom/chatgetter.py index 13e234ee..6bd4c1c3 100644 --- a/telethon/types/_custom/chatgetter.py +++ b/telethon/types/_custom/chatgetter.py @@ -64,12 +64,6 @@ class ChatGetter(abc.ABC): Note that this might not be available if the library doesn't have enough information available. """ - if self._input_chat is None and self._chat_peer and self._client: - try: - self._input_chat = self._client._entity_cache[self._chat_peer] - except KeyError: - pass - return self._input_chat async def get_input_chat(self): diff --git a/telethon/types/_custom/draft.py b/telethon/types/_custom/draft.py index 82e0cb26..46dc9c87 100644 --- a/telethon/types/_custom/draft.py +++ b/telethon/types/_custom/draft.py @@ -49,12 +49,6 @@ class Draft: """ Input version of the entity. """ - if not self._input_entity: - try: - self._input_entity = self._client._entity_cache[self._peer] - except KeyError: - pass - return self._input_entity async def get_entity(self): diff --git a/telethon/types/_custom/forward.py b/telethon/types/_custom/forward.py index c5839c4b..1a4c7672 100644 --- a/telethon/types/_custom/forward.py +++ b/telethon/types/_custom/forward.py @@ -35,13 +35,11 @@ class Forward(ChatGetter, SenderGetter): ty = helpers._entity_type(original.from_id) if ty == helpers._EntityType.USER: sender_id = utils.get_peer_id(original.from_id) - sender, input_sender = utils._get_entity_pair( - sender_id, entities, client._entity_cache) + sender, input_sender = utils._get_entity_pair(sender_id, entities) elif ty in (helpers._EntityType.CHAT, helpers._EntityType.CHANNEL): peer = original.from_id - chat, input_chat = utils._get_entity_pair( - utils.get_peer_id(peer), entities, client._entity_cache) + chat, input_chat = utils._get_entity_pair(utils.get_peer_id(peer), entities) # This call resets the client ChatGetter.__init__(self, peer, chat=chat, input_chat=input_chat) diff --git a/telethon/types/_custom/message.py b/telethon/types/_custom/message.py index 54dde6b0..c1e213aa 100644 --- a/telethon/types/_custom/message.py +++ b/telethon/types/_custom/message.py @@ -435,20 +435,15 @@ class Message(ChatGetter, SenderGetter): if self.peer_id == _tl.PeerUser(client._session_state.user_id) and not self.fwd_from: self.out = True - cache = client._entity_cache + self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, entities) - self._sender, self._input_sender = utils._get_entity_pair( - self.sender_id, entities, cache) - - self._chat, self._input_chat = utils._get_entity_pair( - self.chat_id, entities, cache) + self._chat, self._input_chat = utils._get_entity_pair(self.chat_id, entities) if input_chat: # This has priority self._input_chat = input_chat if self.via_bot_id: - self._via_bot, self._via_input_bot = utils._get_entity_pair( - self.via_bot_id, entities, cache) + self._via_bot, self._via_input_bot = utils._get_entity_pair(self.via_bot_id, entities) if self.fwd_from: self._forward = Forward(self._client, self.fwd_from, entities) @@ -1339,10 +1334,7 @@ class Message(ChatGetter, SenderGetter): raise ValueError('No input sender') return bot else: - try: - return self._client._entity_cache[self.via_bot_id] - except KeyError: - raise ValueError('No input sender') from None + raise ValueError('No input sender') from None def _document_by_attribute(self, kind, condition=None): """ diff --git a/telethon/types/_custom/sendergetter.py b/telethon/types/_custom/sendergetter.py index 673cab25..58d84657 100644 --- a/telethon/types/_custom/sendergetter.py +++ b/telethon/types/_custom/sendergetter.py @@ -64,12 +64,6 @@ class SenderGetter(abc.ABC): Note that this might not be available if the library can't find the input chat, or if the message a broadcast on a channel. """ - if self._input_sender is None and self._sender_id and self._client: - try: - self._input_sender = \ - self._client._entity_cache[self._sender_id] - except KeyError: - pass return self._input_sender async def get_input_sender(self): From 8fc08a0c96804c34b2877db8724843ccc1ff67ea Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 18 Jan 2022 18:16:21 +0100 Subject: [PATCH 147/256] Remove remaining self._all_dcs uses --- telethon/_client/telegrambaseclient.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 9ed5cd71..c8c60df1 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -210,7 +210,7 @@ def set_flood_sleep_threshold(self, value): async def connect(self: 'TelegramClient') -> None: - self._all_dcs = {dc.id: dc for dc in await self._session.get_all_dc()} + all_dcs = {dc.id: dc for dc in await self._session.get_all_dc()} self._session_state = await self._session.get_state() if self._session_state is None: @@ -228,7 +228,7 @@ async def connect(self: 'TelegramClient') -> None: else: try_fetch_user = self._session_state.user_id == 0 - dc = self._all_dcs.get(self._session_state.dc_id) + dc = all_dcs.get(self._session_state.dc_id) if dc is None: dc = DataCenter( id=DEFAULT_DC_ID, @@ -237,7 +237,7 @@ async def connect(self: 'TelegramClient') -> None: port=DEFAULT_PORT, auth=b'', ) - self._all_dcs[dc.id] = dc + all_dcs[dc.id] = dc # Update state (for catching up after a disconnection) # TODO Get state from channels too @@ -257,7 +257,7 @@ async def connect(self: 'TelegramClient') -> None: return if self._sender.auth_key.key != dc.auth: - self._all_dcs[dc.id] = dc = dataclasses.replace(dc, auth=self._sender.auth_key.key) + all_dcs[dc.id] = dc = dataclasses.replace(dc, auth=self._sender.auth_key.key) # Need to send invokeWithLayer for things to work out. # Make the most out of this opportunity by also refreshing our state. @@ -273,17 +273,17 @@ async def connect(self: 'TelegramClient') -> None: continue ip = int(ipaddress.ip_address(dc.ip_address)) - if dc.id in self._all_dcs: + if dc.id in all_dcs: if dc.ipv6: - self._all_dcs[dc.id] = dataclasses.replace(self._all_dcs[dc.id], port=dc.port, ipv6=ip) + all_dcs[dc.id] = dataclasses.replace(all_dcs[dc.id], port=dc.port, ipv6=ip) else: - self._all_dcs[dc.id] = dataclasses.replace(self._all_dcs[dc.id], port=dc.port, ipv4=ip) + all_dcs[dc.id] = dataclasses.replace(all_dcs[dc.id], port=dc.port, ipv4=ip) elif dc.ipv6: - self._all_dcs[dc.id] = DataCenter(dc.id, None, ip, dc.port, b'') + all_dcs[dc.id] = DataCenter(dc.id, None, ip, dc.port, b'') else: - self._all_dcs[dc.id] = DataCenter(dc.id, ip, None, dc.port, b'') + all_dcs[dc.id] = DataCenter(dc.id, ip, None, dc.port, b'') - for dc in self._all_dcs.values(): + for dc in all_dcs.values(): await self._session.insert_dc(dc) if try_fetch_user: @@ -384,7 +384,7 @@ async def _create_exported_sender(self: 'TelegramClient', dc_id): """ # Thanks badoualy/kotlogram on /telegram/api/DefaultTelegramClient.kt # for clearly showing how to export the authorization - dc = self._all_dcs[dc_id] + dc = next(dc for dc in await self._session.get_all_dc() if dc.id == dc_id) # Can't reuse self._sender._connection as it has its own seqno. # # If one were to do that, Telegram would reset the connection @@ -423,7 +423,7 @@ async def _borrow_exported_sender(self: 'TelegramClient', dc_id): self._borrowed_senders[dc_id] = (state, sender) elif state.need_connect(): - dc = self._all_dcs[dc_id] + dc = next(dc for dc in await self._session.get_all_dc() if dc.id == dc_id) await self._sender.connect(Connection( ip=str(ipaddress.ip_address((self._use_ipv6 and dc.ipv6) or dc.ipv4)), From 7142734fb433c7867cd4f790aa26b50afa0dee87 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 18 Jan 2022 18:19:14 +0100 Subject: [PATCH 148/256] Remove StateCache and EntityCache --- telethon/_client/auth.py | 2 - telethon/_client/telegrambaseclient.py | 8 -- telethon/_client/updates.py | 15 +-- telethon/_misc/entitycache.py | 179 ------------------------- telethon/_misc/statecache.py | 164 ---------------------- 5 files changed, 2 insertions(+), 366 deletions(-) delete mode 100644 telethon/_misc/entitycache.py delete mode 100644 telethon/_misc/statecache.py diff --git a/telethon/_client/auth.py b/telethon/_client/auth.py index 69b5df7e..7d122d8f 100644 --- a/telethon/_client/auth.py +++ b/telethon/_client/auth.py @@ -375,8 +375,6 @@ async def log_out(self: 'TelegramClient') -> bool: except errors.RPCError: return False - self._state_cache.reset() - await self.disconnect() return True diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index c8c60df1..9fbf3f14 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -239,10 +239,6 @@ async def connect(self: 'TelegramClient') -> None: ) all_dcs[dc.id] = dc - # Update state (for catching up after a disconnection) - # TODO Get state from channels too - self._state_cache = statecache.StateCache(self._session_state, self._log) - # Use known key, if any self._sender.auth_key.key = dc.auth @@ -351,10 +347,6 @@ async def _disconnect_coro(self: 'TelegramClient'): await asyncio.wait(self._updates_queue) self._updates_queue.clear() - pts, date = self._state_cache[None] - if pts and date: - if self._session_state: - await self._replace_session_state(pts=pts, date=date) async def _disconnect(self: 'TelegramClient'): """ diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index 141f6b95..92515dae 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -79,10 +79,7 @@ def list_event_handlers(self: 'TelegramClient')\ return [(callback, event) for event, callback in self._event_builders] async def catch_up(self: 'TelegramClient'): - pts, date = self._state_cache[None] - if not pts: - return - + return self._catching_up = True try: while True: @@ -131,8 +128,6 @@ async def catch_up(self: 'TelegramClient'): except (ConnectionError, asyncio.CancelledError): pass finally: - # TODO Save new pts to session - self._state_cache._pts_date = (pts, date) self._catching_up = False @@ -150,14 +145,12 @@ def _handle_update(self: 'TelegramClient', update): else: _process_update(self, update, {}, None) - self._state_cache.update(update) def _process_update(self: 'TelegramClient', update, entities, others): # This part is somewhat hot so we don't bother patching # update with channel ID/its state. Instead we just pass # arguments which is faster. - channel_id = self._state_cache.get_channel_id(update) - args = (update, entities, others, channel_id, self._state_cache[channel_id]) + args = (update, entities, others, channel_id, None) if self._dispatching_updates_queue is None: task = asyncio.create_task(_dispatch_update(self, *args)) self._updates_queue.add(task) @@ -168,8 +161,6 @@ def _process_update(self: 'TelegramClient', update, entities, others): self._dispatching_updates_queue.set() asyncio.create_task(_dispatch_queue_updates(self)) - self._state_cache.update(update) - async def _update_loop(self: 'TelegramClient'): # Pings' ID don't really need to be secure, just "random" rnd = lambda: random.randrange(-2**63, 2**63) @@ -326,7 +317,6 @@ async def _get_difference(self: 'TelegramClient', update, entities, channel_id, result = await self(_tl.fn.channels.GetFullChannel( utils.get_input_channel(where) )) - self._state_cache[channel_id] = result.full_chat.pts return result = await self(_tl.fn.updates.GetChannelDifference( @@ -340,7 +330,6 @@ async def _get_difference(self: 'TelegramClient', update, entities, channel_id, if not pts_date[0]: # First-time, can't get difference. Get pts instead. result = await self(_tl.fn.updates.GetState()) - self._state_cache[None] = result.pts, result.date return result = await self(_tl.fn.updates.GetDifference( diff --git a/telethon/_misc/entitycache.py b/telethon/_misc/entitycache.py deleted file mode 100644 index 2b7b8af1..00000000 --- a/telethon/_misc/entitycache.py +++ /dev/null @@ -1,179 +0,0 @@ -import inspect -import itertools - -from .._misc import utils -from .. import _tl -from .._sessions.types import EntityType, Entity - -# Which updates have the following fields? -_has_field = { - ('user_id', int): [], - ('chat_id', int): [], - ('channel_id', int): [], - ('peer', 'TypePeer'): [], - ('peer', 'TypeDialogPeer'): [], - ('message', 'TypeMessage'): [], -} - -# Note: We don't bother checking for some rare: -# * `UpdateChatParticipantAdd.inviter_id` integer. -# * `UpdateNotifySettings.peer` dialog peer. -# * `UpdatePinnedDialogs.order` list of dialog peers. -# * `UpdateReadMessagesContents.messages` list of messages. -# * `UpdateChatParticipants.participants` list of participants. -# -# There are also some uninteresting `update.message` of type string. - - -def _fill(): - for name in dir(_tl): - update = getattr(_tl, name) - if getattr(update, 'SUBCLASS_OF_ID', None) == 0x9f89304e: - cid = update.CONSTRUCTOR_ID - sig = inspect.signature(update.__init__) - for param in sig.parameters.values(): - vec = _has_field.get((param.name, param.annotation)) - if vec is not None: - vec.append(cid) - - # Future-proof check: if the documentation format ever changes - # then we won't be able to pick the update types we are interested - # in, so we must make sure we have at least an update for each field - # which likely means we are doing it right. - if not all(_has_field.values()): - raise RuntimeError('FIXME: Did the init signature or updates change?') - - -# We use a function to avoid cluttering the globals (with name/update/cid/doc) -_fill() - - -class EntityCache: - """ - In-memory input entity cache, defaultdict-like behaviour. - """ - def add(self, entities, _mappings={ - _tl.User.CONSTRUCTOR_ID: lambda e: (EntityType.BOT if e.bot else EntityType.USER, e.id, e.access_hash), - _tl.UserFull.CONSTRUCTOR_ID: lambda e: (EntityType.BOT if e.user.bot else EntityType.USER, e.user.id, e.user.access_hash), - _tl.Chat.CONSTRUCTOR_ID: lambda e: (EntityType.GROUP, e.id, 0), - _tl.ChatFull.CONSTRUCTOR_ID: lambda e: (EntityType.GROUP, e.id, 0), - _tl.ChatEmpty.CONSTRUCTOR_ID: lambda e: (EntityType.GROUP, e.id, 0), - _tl.ChatForbidden.CONSTRUCTOR_ID: lambda e: (EntityType.GROUP, e.id, 0), - _tl.Channel.CONSTRUCTOR_ID: lambda e: ( - EntityType.MEGAGROUP if e.megagroup else (EntityType.GIGAGROUP if e.gigagroup else EntityType.CHANNEL), - e.id, - e.access_hash, - ), - _tl.ChannelForbidden.CONSTRUCTOR_ID: lambda e: (EntityType.MEGAGROUP if e.megagroup else EntityType.CHANNEL, e.id, e.access_hash), - }): - """ - Adds the given entities to the cache, if they weren't saved before. - - Returns a list of Entity that can be saved in the session. - """ - if not utils.is_list_like(entities): - # Invariant: all "chats" and "users" are always iterables, - # and "user" and "chat" never are (so we wrap them inside a list). - # - # Itself may be already the entity we want to cache. - entities = itertools.chain( - [entities], - getattr(entities, 'chats', []), - getattr(entities, 'users', []), - (hasattr(entities, 'user') and [entities.user]) or [], - (hasattr(entities, 'chat') and [entities.user]) or [], - ) - - rows = [] - for e in entities: - try: - mapper = _mappings[e.CONSTRUCTOR_ID] - except (AttributeError, KeyError): - continue - - ty, id, access_hash = mapper(e) - - # Need to check for non-zero access hash unless it's a group (#354 and #392). - # Also check it's not `min` (`access_hash` usage is limited since layer 102). - if not getattr(e, 'min', False) and (access_hash or ty == Entity.GROUP): - rows.append(Entity(ty, id, access_hash)) - if id not in self.__dict__: - if ty in (EntityType.USER, EntityType.BOT): - self.__dict__[id] = _tl.InputPeerUser(id, access_hash) - elif ty in (EntityType.GROUP,): - self.__dict__[id] = _tl.InputPeerChat(id) - elif ty in (EntityType.CHANNEL, EntityType.MEGAGROUP, EntityType.GIGAGROUP): - self.__dict__[id] = _tl.InputPeerChannel(id, access_hash) - - return rows - - def __getitem__(self, item): - """ - Gets the corresponding :tl:`InputPeer` for the given ID or peer, - or raises ``KeyError`` on any error (i.e. cannot be found). - """ - if not isinstance(item, int) or item < 0: - try: - return self.__dict__[utils.get_peer_id(item)] - except TypeError: - raise KeyError('Invalid key will not have entity') from None - - for cls in (_tl.PeerUser, _tl.PeerChat, _tl.PeerChannel): - result = self.__dict__.get(utils.get_peer_id(cls(item))) - if result: - return result - - raise KeyError('No cached entity for the given key') - - def clear(self): - """ - Clear the entity cache. - """ - self.__dict__.clear() - - def ensure_cached( - self, - update, - has_user_id=frozenset(_has_field[('user_id', int)]), - has_chat_id=frozenset(_has_field[('chat_id', int)]), - has_channel_id=frozenset(_has_field[('channel_id', int)]), - has_peer=frozenset(_has_field[('peer', 'TypePeer')] + _has_field[('peer', 'TypeDialogPeer')]), - has_message=frozenset(_has_field[('message', 'TypeMessage')]) - ): - """ - Ensures that all the relevant entities in the given update are cached. - """ - # This method is called pretty often and we want it to have the lowest - # overhead possible. For that, we avoid `isinstance` and constantly - # getting attributes out of `_tl.` by "caching" the constructor IDs - # in sets inside the arguments, and using local variables. - dct = self.__dict__ - cid = update.CONSTRUCTOR_ID - if cid in has_user_id and \ - update.user_id not in dct: - return False - - if cid in has_chat_id and update.chat_id not in dct: - return False - - if cid in has_channel_id and update.channel_id not in dct: - return False - - if cid in has_peer and \ - utils.get_peer_id(update.peer) not in dct: - return False - - if cid in has_message: - x = update.message - y = getattr(x, 'peer_id', None) # handle MessageEmpty - if y and utils.get_peer_id(y) not in dct: - return False - - y = getattr(x, 'from_id', None) - if y and utils.get_peer_id(y) not in dct: - return False - - # We don't quite worry about entities anywhere else. - # This is enough. - - return True diff --git a/telethon/_misc/statecache.py b/telethon/_misc/statecache.py deleted file mode 100644 index c1a6d7c9..00000000 --- a/telethon/_misc/statecache.py +++ /dev/null @@ -1,164 +0,0 @@ -import inspect - -from .. import _tl - - -# Which updates have the following fields? -_has_channel_id = [] - - -# TODO EntityCache does the same. Reuse? -def _fill(): - for name in dir(_tl): - update = getattr(_tl, name) - if getattr(update, 'SUBCLASS_OF_ID', None) == 0x9f89304e: - cid = update.CONSTRUCTOR_ID - sig = inspect.signature(update.__init__) - for param in sig.parameters.values(): - if param.name == 'channel_id' and param.annotation == int: - _has_channel_id.append(cid) - - if not _has_channel_id: - raise RuntimeError('FIXME: Did the init signature or updates change?') - - -# We use a function to avoid cluttering the globals (with name/update/cid/doc) -_fill() - - -class StateCache: - """ - In-memory update state cache, defaultdict-like behaviour. - """ - def __init__(self, initial, loggers): - # We only care about the pts and the date. By using a tuple which - # is lightweight and immutable we can easily copy them around to - # each update in case they need to fetch missing entities. - self._logger = loggers[__name__] - if initial: - self._pts_date = initial.pts or None, initial.date or None - else: - self._pts_date = None, None - - def reset(self): - self.__dict__.clear() - self._pts_date = None, None - - # TODO Call this when receiving responses too...? - def update( - self, - update, - *, - channel_id=None, - has_pts=frozenset(x.CONSTRUCTOR_ID for x in ( - _tl.UpdateNewMessage, - _tl.UpdateDeleteMessages, - _tl.UpdateReadHistoryInbox, - _tl.UpdateReadHistoryOutbox, - _tl.UpdateWebPage, - _tl.UpdateReadMessagesContents, - _tl.UpdateEditMessage, - _tl.updates.State, - _tl.updates.DifferenceTooLong, - _tl.UpdateShortMessage, - _tl.UpdateShortChatMessage, - _tl.UpdateShortSentMessage - )), - has_date=frozenset(x.CONSTRUCTOR_ID for x in ( - _tl.UpdateUserPhoto, - _tl.UpdateEncryption, - _tl.UpdateEncryptedMessagesRead, - _tl.UpdateChatParticipantAdd, - _tl.updates.DifferenceEmpty, - _tl.UpdateShortMessage, - _tl.UpdateShortChatMessage, - _tl.UpdateShort, - _tl.UpdatesCombined, - _tl.Updates, - _tl.UpdateShortSentMessage, - )), - has_channel_pts=frozenset(x.CONSTRUCTOR_ID for x in ( - _tl.UpdateChannelTooLong, - _tl.UpdateNewChannelMessage, - _tl.UpdateDeleteChannelMessages, - _tl.UpdateEditChannelMessage, - _tl.UpdateChannelWebPage, - _tl.updates.ChannelDifferenceEmpty, - _tl.updates.ChannelDifferenceTooLong, - _tl.updates.ChannelDifference - )), - check_only=False - ): - """ - Update the state with the given update. - """ - cid = update.CONSTRUCTOR_ID - if check_only: - return cid in has_pts or cid in has_date or cid in has_channel_pts - - if cid in has_pts: - if cid in has_date: - self._pts_date = update.pts, update.date - else: - self._pts_date = update.pts, self._pts_date[1] - elif cid in has_date: - self._pts_date = self._pts_date[0], update.date - - if cid in has_channel_pts: - if channel_id is None: - channel_id = self.get_channel_id(update) - - if channel_id is None: - self._logger.info( - 'Failed to retrieve channel_id from %s', update) - else: - self.__dict__[channel_id] = update.pts - - def get_channel_id( - self, - update, - has_channel_id=frozenset(_has_channel_id), - # Hardcoded because only some with message are for channels - has_message=frozenset(x.CONSTRUCTOR_ID for x in ( - _tl.UpdateNewChannelMessage, - _tl.UpdateEditChannelMessage - )) - ): - """ - Gets the **unmarked** channel ID from this update, if it has any. - - Fails for ``*difference`` updates, where ``channel_id`` - is supposedly already known from the outside. - """ - cid = update.CONSTRUCTOR_ID - if cid in has_channel_id: - return update.channel_id - elif cid in has_message: - if update.message.peer_id is None: - # Telegram sometimes sends empty messages to give a newer pts: - # UpdateNewChannelMessage(message=MessageEmpty(id), pts=pts, pts_count=1) - # Not sure why, but it's safe to ignore them. - self._logger.debug('Update has None peer_id %s', update) - else: - return update.message.peer_id.channel_id - - return None - - def __getitem__(self, item): - """ - If `item` is `None`, returns the default ``(pts, date)``. - - If it's an **unmarked** channel ID, returns its ``pts``. - - If no information is known, ``pts`` will be `None`. - """ - if item is None: - return self._pts_date - else: - return self.__dict__.get(item) - - def __setitem__(self, where, value): - if where is None: - self._pts_date = value - else: - self.__dict__[where] = value From 3afabdd7c05eba244a047f10ed5a00aac02541e4 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 18 Jan 2022 18:21:56 +0100 Subject: [PATCH 149/256] Remove auto-reconnect callback It's an abstraction leak. The client should know to refetch updates if a long period passed without them on its own. --- telethon/_client/telegrambaseclient.py | 1 - telethon/_client/telegramclient.py | 4 --- telethon/_client/updates.py | 44 -------------------------- telethon/_network/mtprotosender.py | 7 +--- 4 files changed, 1 insertion(+), 55 deletions(-) diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 9fbf3f14..62dc09b0 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -190,7 +190,6 @@ def init( auto_reconnect=self._auto_reconnect, connect_timeout=self._connect_timeout, update_callback=self._handle_update, - auto_reconnect_callback=self._handle_auto_reconnect ) # Cache ``{dc_id: (_ExportState, MTProtoSender)}`` for all borrowed senders. diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 5f555b06..412e7b61 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -3513,10 +3513,6 @@ class TelegramClient: def _handle_update(self: 'TelegramClient', update): pass - @forward_call(updates._handle_auto_reconnect) - async def _handle_auto_reconnect(self: 'TelegramClient'): - pass - @forward_call(auth._update_session_state) async def _update_session_state(self, user, *, save=True): pass diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index 92515dae..4b8e29cb 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -347,50 +347,6 @@ async def _get_difference(self: 'TelegramClient', update, entities, channel_id, itertools.chain(result.users, result.chats) }) -async def _handle_auto_reconnect(self: 'TelegramClient'): - # TODO Catch-up - # For now we make a high-level request to let Telegram - # know we are still interested in receiving more updates. - try: - await self.get_me() - except Exception as e: - self._log[__name__].warning('Error executing high-level request ' - 'after reconnect: %s: %s', type(e), e) - - return - try: - self._log[__name__].info( - 'Asking for the current state after reconnect...') - - # TODO consider: - # If there aren't many updates while the client is disconnected - # (I tried with up to 20), Telegram seems to send them without - # asking for them (via updates.getDifference). - # - # On disconnection, the library should probably set a "need - # difference" or "catching up" flag so that any new updates are - # ignored, and then the library should call updates.getDifference - # itself to fetch them. - # - # In any case (either there are too many updates and Telegram - # didn't send them, or there isn't a lot and Telegram sent them - # but we dropped them), we fetch the new difference to get all - # missed updates. I feel like this would be the best solution. - - # If a disconnection occurs, the old known state will be - # the latest one we were aware of, so we can catch up since - # the most recent state we were aware of. - await self.catch_up() - - self._log[__name__].info('Successfully fetched missed updates') - except RpcError as e: - self._log[__name__].warning('Failed to get missed updates after ' - 'reconnect: %r', e) - except Exception: - self._log[__name__].exception( - 'Unhandled exception while getting update difference after reconnect') - - class EventBuilderDict: """ Helper "dictionary" to return events from types and cache them. diff --git a/telethon/_network/mtprotosender.py b/telethon/_network/mtprotosender.py index 92438502..b05137e3 100644 --- a/telethon/_network/mtprotosender.py +++ b/telethon/_network/mtprotosender.py @@ -37,7 +37,7 @@ class MTProtoSender: """ def __init__(self, *, loggers, retries=5, delay=1, auto_reconnect=True, connect_timeout=None, - update_callback=None, auto_reconnect_callback=None): + update_callback=None): self._connection = None self._loggers = loggers self._log = loggers[__name__] @@ -46,7 +46,6 @@ class MTProtoSender: self._auto_reconnect = auto_reconnect self._connect_timeout = connect_timeout self._update_callback = update_callback - self._auto_reconnect_callback = auto_reconnect_callback self._connect_lock = asyncio.Lock() self._ping = None @@ -373,10 +372,6 @@ class MTProtoSender: else: self._send_queue.extend(self._pending_state.values()) self._pending_state.clear() - - if self._auto_reconnect_callback: - asyncio.create_task(self._auto_reconnect_callback()) - break else: ok = False From f6df5d377c78f0dbc2559b14df159b9b28b0303d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 18 Jan 2022 19:46:19 +0100 Subject: [PATCH 150/256] Begin reworking update handling Use a fixed-size queue instead of a callback to deal with updates. Port the message box and entity cache from grammers to start off with a clean design. Temporarily get rid of other cruft such as automatic pings or old catch up implementation. --- telethon/_client/telegrambaseclient.py | 32 +- telethon/_client/telegramclient.py | 5 +- telethon/_client/updates.py | 292 +------------ telethon/_network/mtprotosender.py | 26 +- telethon/_updates/__init__.py | 2 + telethon/_updates/entitycache.py | 97 +++++ telethon/_updates/messagebox.py | 565 +++++++++++++++++++++++++ 7 files changed, 704 insertions(+), 315 deletions(-) create mode 100644 telethon/_updates/__init__.py create mode 100644 telethon/_updates/entitycache.py create mode 100644 telethon/_updates/messagebox.py diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 62dc09b0..b6872dc8 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -11,10 +11,11 @@ import dataclasses from .. import version, __name__ as __base_name__, _tl from .._crypto import rsa -from .._misc import markdown, statecache, enums, helpers +from .._misc import markdown, enums, helpers from .._network import MTProtoSender, Connection, transports from .._sessions import Session, SQLiteSession, MemorySession from .._sessions.types import DataCenter, SessionState +from .._updates import EntityCache, MessageBox DEFAULT_DC_ID = 2 DEFAULT_IPV4_IP = '149.154.167.51' @@ -91,6 +92,7 @@ def init( flood_sleep_threshold: int = 60, # Update handling. receive_updates: bool = True, + max_queued_updates: int = 100, ): # Logging. if isinstance(base_logger, str): @@ -139,6 +141,13 @@ def init( self._flood_waited_requests = {} # prevent calls that would floodwait entirely self._parse_mode = markdown + # Update handling. + self._no_updates = not receive_updates + self._updates_queue = asyncio.Queue(maxsize=max_queued_updates) + self._updates_handle = None + self._message_box = MessageBox() + self._entity_cache = EntityCache() # required for proper update handling (to know when to getDifference) + # Connection parameters. if not api_id or not api_hash: raise ValueError( @@ -189,16 +198,13 @@ def init( delay=self._connect_retry_delay, auto_reconnect=self._auto_reconnect, connect_timeout=self._connect_timeout, - update_callback=self._handle_update, + updates_queue=self._updates_queue, ) # Cache ``{dc_id: (_ExportState, MTProtoSender)}`` for all borrowed senders. self._borrowed_senders = {} self._borrow_sender_lock = asyncio.Lock() - # Update handling. - self._no_updates = not receive_updates - def get_flood_sleep_threshold(self): return self._flood_sleep_threshold @@ -337,15 +343,6 @@ async def _disconnect_coro(self: 'TelegramClient'): # If any was borrowed self._borrowed_senders.clear() - # trio's nurseries would handle this for us, but this is asyncio. - # All tasks spawned in the background should properly be terminated. - if self._dispatching_updates_queue is None and self._updates_queue: - for task in self._updates_queue: - task.cancel() - - await asyncio.wait(self._updates_queue) - self._updates_queue.clear() - async def _disconnect(self: 'TelegramClient'): """ @@ -355,8 +352,11 @@ async def _disconnect(self: 'TelegramClient'): their job with the client is complete and we should clean it up all. """ await self._sender.disconnect() - await helpers._cancel(self._log[__name__], - updates_handle=self._updates_handle) + await helpers._cancel(self._log[__name__], updates_handle=self._updates_handle) + try: + await self._updates_handle + except asyncio.CancelledError: + pass async def _switch_dc(self: 'TelegramClient', new_dc): """ diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 412e7b61..4504d589 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -2665,6 +2665,7 @@ class TelegramClient: flood_sleep_threshold: int = 60, # Update handling. receive_updates: bool = True, + max_queued_updates: int = 100, ): telegrambaseclient.init(**locals()) @@ -3509,10 +3510,6 @@ class TelegramClient: async def _clean_exported_senders(self: 'TelegramClient'): pass - @forward_call(updates._handle_update) - def _handle_update(self: 'TelegramClient', update): - pass - @forward_call(auth._update_session_state) async def _update_session_state(self, user, *, save=True): pass diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index 4b8e29cb..2460c561 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -79,295 +79,9 @@ def list_event_handlers(self: 'TelegramClient')\ return [(callback, event) for event, callback in self._event_builders] async def catch_up(self: 'TelegramClient'): - return - self._catching_up = True - try: - while True: - d = await self(_tl.fn.updates.GetDifference( - pts, date, 0 - )) - if isinstance(d, (_tl.updates.DifferenceSlice, - _tl.updates.Difference)): - if isinstance(d, _tl.updates.Difference): - state = d.state - else: - state = d.intermediate_state - - pts, date = state.pts, state.date - _handle_update(self, _tl.Updates( - users=d.users, - chats=d.chats, - date=state.date, - seq=state.seq, - updates=d.other_updates + [ - _tl.UpdateNewMessage(m, 0, 0) - for m in d.new_messages - ] - )) - - # TODO Implement upper limit (max_pts) - # We don't want to fetch updates we already know about. - # - # We may still get duplicates because the Difference - # contains a lot of updates and presumably only has - # the state for the last one, but at least we don't - # unnecessarily fetch too many. - # - # updates.getDifference's pts_total_limit seems to mean - # "how many pts is the request allowed to return", and - # if there is more than that, it returns "too long" (so - # there would be duplicate updates since we know about - # some). This can be used to detect collisions (i.e. - # it would return an update we have already seen). - else: - if isinstance(d, _tl.updates.DifferenceEmpty): - date = d.date - elif isinstance(d, _tl.updates.DifferenceTooLong): - pts = d.pts - break - except (ConnectionError, asyncio.CancelledError): - pass - finally: - self._catching_up = False - - -# It is important to not make _handle_update async because we rely on -# the order that the updates arrive in to update the pts and date to -# be always-increasing. There is also no need to make this async. -def _handle_update(self: 'TelegramClient', update): - if isinstance(update, (_tl.Updates, _tl.UpdatesCombined)): - entities = {utils.get_peer_id(x): x for x in - itertools.chain(update.users, update.chats)} - for u in update.updates: - _process_update(self, u, entities, update.updates) - elif isinstance(update, _tl.UpdateShort): - _process_update(self, update.update, {}, None) - else: - _process_update(self, update, {}, None) - - -def _process_update(self: 'TelegramClient', update, entities, others): - # This part is somewhat hot so we don't bother patching - # update with channel ID/its state. Instead we just pass - # arguments which is faster. - args = (update, entities, others, channel_id, None) - if self._dispatching_updates_queue is None: - task = asyncio.create_task(_dispatch_update(self, *args)) - self._updates_queue.add(task) - task.add_done_callback(lambda _: self._updates_queue.discard(task)) - else: - self._updates_queue.put_nowait(args) - if not self._dispatching_updates_queue.is_set(): - self._dispatching_updates_queue.set() - asyncio.create_task(_dispatch_queue_updates(self)) + pass async def _update_loop(self: 'TelegramClient'): - # Pings' ID don't really need to be secure, just "random" - rnd = lambda: random.randrange(-2**63, 2**63) while self.is_connected(): - try: - await asyncio.wait_for(self.run_until_disconnected(), timeout=60) - continue # We actually just want to act upon timeout - except asyncio.TimeoutError: - pass - except asyncio.CancelledError: - return - except Exception as e: - # Any disconnected exception should be ignored (or it may hint at - # another problem, leading to an infinite loop, hence the logging call) - self._log[__name__].info('Exception waiting on a disconnect: %s', e) - continue - - # Check if we have any exported senders to clean-up periodically - await self._clean_exported_senders() - - # Don't bother sending pings until the low-level connection is - # ready, otherwise a lot of pings will be batched to be sent upon - # reconnect, when we really don't care about that. - if not self._sender._transport_connected(): - continue - - # We also don't really care about their result. - # Just send them periodically. - try: - self._sender._keepalive_ping(rnd()) - except (ConnectionError, asyncio.CancelledError): - return - - # Entities are not saved when they are inserted because this is a rather expensive - # operation (default's sqlite3 takes ~0.1s to commit changes). Do it every minute - # instead. No-op if there's nothing new. - await self._session.save() - - # We need to send some content-related request at least hourly - # for Telegram to keep delivering updates, otherwise they will - # just stop even if we're connected. Do so every 30 minutes. - # - # TODO Call getDifference instead since it's more relevant - if time.time() - self._last_request > 30 * 60: - if not await self.is_user_authorized(): - # What can be the user doing for so - # long without being logged in...? - continue - - try: - await self(_tl.fn.updates.GetState()) - except (ConnectionError, asyncio.CancelledError): - return - -async def _dispatch_queue_updates(self: 'TelegramClient'): - while not self._updates_queue.empty(): - await _dispatch_update(self, *self._updates_queue.get_nowait()) - - self._dispatching_updates_queue.clear() - -async def _dispatch_update(self: 'TelegramClient', update, entities, others, channel_id, pts_date): - built = EventBuilderDict(self, update, entities, others) - - for builder, callback in self._event_builders: - event = built[type(builder)] - if not event: - continue - - if not builder.resolved: - await builder.resolve(self) - - filter = builder.filter(event) - if inspect.isawaitable(filter): - filter = await filter - if not filter: - continue - - try: - await callback(event) - except StopPropagation: - name = getattr(callback, '__name__', repr(callback)) - self._log[__name__].debug( - 'Event handler "%s" stopped chain of propagation ' - 'for event %s.', name, type(event).__name__ - ) - break - except Exception as e: - if not isinstance(e, asyncio.CancelledError) or self.is_connected(): - name = getattr(callback, '__name__', repr(callback)) - self._log[__name__].exception('Unhandled exception on %s', name) - -async def _dispatch_event(self: 'TelegramClient', event): - """ - Dispatches a single, out-of-order event. Used by `AlbumHack`. - """ - # We're duplicating a most logic from `_dispatch_update`, but all in - # the name of speed; we don't want to make it worse for all updates - # just because albums may need it. - for builder, callback in self._event_builders: - if isinstance(builder, Raw): - continue - if not isinstance(event, builder.Event): - continue - - if not builder.resolved: - await builder.resolve(self) - - filter = builder.filter(event) - if inspect.isawaitable(filter): - filter = await filter - if not filter: - continue - - try: - await callback(event) - except StopPropagation: - name = getattr(callback, '__name__', repr(callback)) - self._log[__name__].debug( - 'Event handler "%s" stopped chain of propagation ' - 'for event %s.', name, type(event).__name__ - ) - break - except Exception as e: - if not isinstance(e, asyncio.CancelledError) or self.is_connected(): - name = getattr(callback, '__name__', repr(callback)) - self._log[__name__].exception('Unhandled exception on %s', name) - -async def _get_difference(self: 'TelegramClient', update, entities, channel_id, pts_date): - """ - Get the difference for this `channel_id` if any, then load entities. - - Calls :tl:`updates.getDifference`, which fills the entities cache - (always done by `__call__`) and lets us know about the full entities. - """ - # Fetch since the last known pts/date before this update arrived, - # in order to fetch this update at full, including its entities. - self._log[__name__].debug('Getting difference for entities ' - 'for %r', update.__class__) - if channel_id: - # There are reports where we somehow call get channel difference - # with `InputPeerEmpty`. Check our assumptions to better debug - # this when it happens. - assert isinstance(channel_id, int), 'channel_id was {}, not int in {}'.format(type(channel_id), update) - try: - # Wrap the ID inside a peer to ensure we get a channel back. - where = await self.get_input_entity(_tl.PeerChannel(channel_id)) - except ValueError: - # There's a high chance that this fails, since - # we are getting the difference to fetch entities. - return - - if not pts_date: - # First-time, can't get difference. Get pts instead. - result = await self(_tl.fn.channels.GetFullChannel( - utils.get_input_channel(where) - )) - return - - result = await self(_tl.fn.updates.GetChannelDifference( - channel=where, - filter=_tl.ChannelMessagesFilterEmpty(), - pts=pts_date, # just pts - limit=100, - force=True - )) - else: - if not pts_date[0]: - # First-time, can't get difference. Get pts instead. - result = await self(_tl.fn.updates.GetState()) - return - - result = await self(_tl.fn.updates.GetDifference( - pts=pts_date[0], - date=pts_date[1], - qts=0 - )) - - if isinstance(result, (_tl.updates.Difference, - _tl.updates.DifferenceSlice, - _tl.updates.ChannelDifference, - _tl.updates.ChannelDifferenceTooLong)): - entities.update({ - utils.get_peer_id(x): x for x in - itertools.chain(result.users, result.chats) - }) - -class EventBuilderDict: - """ - Helper "dictionary" to return events from types and cache them. - """ - def __init__(self, client: 'TelegramClient', update, entities, others): - self.client = client - self.update = update - self.entities = entities - self.others = others - - def __getitem__(self, builder): - try: - return self.__dict__[builder] - except KeyError: - event = self.__dict__[builder] = builder.build( - self.update, self.others, self.client._session_state.user_id, self.entities or {}, self.client) - - if isinstance(event, EventCommon): - # TODO eww - event.original_update = self.update - event._entities = self.entities or {} - event._set_client(self.client) - - return event + updates = await self._updates_queue.get() + updates, users, chats = self._message_box.process_updates(updates, self._entity_cache) diff --git a/telethon/_network/mtprotosender.py b/telethon/_network/mtprotosender.py index b05137e3..fa58240f 100644 --- a/telethon/_network/mtprotosender.py +++ b/telethon/_network/mtprotosender.py @@ -1,6 +1,7 @@ import asyncio import collections import struct +import logging from . import authenticator from .._misc.messagepacker import MessagePacker @@ -20,6 +21,9 @@ from .._misc import helpers, utils from .. import _tl +UPDATE_BUFFER_FULL_WARN_DELAY = 15 * 60 + + class MTProtoSender: """ MTProto Mobile Protocol sender @@ -35,9 +39,8 @@ class MTProtoSender: A new authorization key will be generated on connection if no other key exists yet. """ - def __init__(self, *, loggers, - retries=5, delay=1, auto_reconnect=True, connect_timeout=None, - update_callback=None): + def __init__(self, *, loggers, updates_queue, + retries=5, delay=1, auto_reconnect=True, connect_timeout=None,): self._connection = None self._loggers = loggers self._log = loggers[__name__] @@ -45,7 +48,7 @@ class MTProtoSender: self._delay = delay self._auto_reconnect = auto_reconnect self._connect_timeout = connect_timeout - self._update_callback = update_callback + self._updates_queue = updates_queue self._connect_lock = asyncio.Lock() self._ping = None @@ -83,6 +86,9 @@ class MTProtoSender: # is received, but we may still need to resend their state on bad salts. self._last_acks = collections.deque(maxlen=10) + # Last time we warned about the update buffer being full + self._last_update_warn = -UPDATE_BUFFER_FULL_WARN_DELAY + # Jump table from response ID to method that handles it self._handlers = { RpcResult.CONSTRUCTOR_ID: self._handle_rpc_result, @@ -629,8 +635,16 @@ class MTProtoSender: return self._log.debug('Handling update %s', message.obj.__class__.__name__) - if self._update_callback: - self._update_callback(message.obj) + try: + self._updates_queue.put_nowait(message.obj) + except asyncio.QueueFull: + now = asyncio.get_running_loop().time() + if now - self._last_update_warn >= UPDATE_BUFFER_FULL_WARN_DELAY: + self._log.warning( + 'Cannot dispatch update because the buffer capacity of %d was reached', + self._updates_queue.maxsize + ) + self._last_update_warn = now async def _handle_pong(self, message): """ diff --git a/telethon/_updates/__init__.py b/telethon/_updates/__init__.py new file mode 100644 index 00000000..7951c9aa --- /dev/null +++ b/telethon/_updates/__init__.py @@ -0,0 +1,2 @@ +from .entitycache import EntityCache, PackedChat +from .messagebox import MessageBox diff --git a/telethon/_updates/entitycache.py b/telethon/_updates/entitycache.py new file mode 100644 index 00000000..176d2013 --- /dev/null +++ b/telethon/_updates/entitycache.py @@ -0,0 +1,97 @@ +import inspect +import itertools +from dataclasses import dataclass, field +from collections import namedtuple + +from .._misc import utils +from .. import _tl +from .._sessions.types import EntityType, Entity + + +class PackedChat(namedtuple('PackedChat', 'ty id hash')): + __slots__ = () + + @property + def is_user(self): + return self.ty in (EntityType.USER, EntityType.BOT) + + @property + def is_chat(self): + return self.ty in (EntityType.GROUP,) + + @property + def is_channel(self): + return self.ty in (EntityType.CHANNEL, EntityType.MEGAGROUP, EntityType.GIGAGROUP) + + def to_peer(self): + if self.is_user: + return _tl.PeerUser(user_id=self.id) + elif self.is_chat: + return _tl.PeerChat(chat_id=self.id) + elif self.is_channel: + return _tl.PeerChannel(channel_id=self.id) + + def to_input_peer(self): + if self.is_user: + return _tl.InputPeerUser(user_id=self.id, access_hash=self.hash) + elif self.is_chat: + return _tl.InputPeerChat(chat_id=self.id) + elif self.is_channel: + return _tl.InputPeerChannel(channel_id=self.id, access_hash=self.hash) + + def try_to_input_user(self): + if self.is_user: + return _tl.InputUser(user_id=self.id, access_hash=self.hash) + else: + return None + + def try_to_chat_id(self): + if self.is_chat: + return self.id + else: + return None + + def try_to_input_channel(self): + if self.is_channel: + return _tl.InputChannel(channel_id=self.id, access_hash=self.hash) + else: + return None + + def __str__(self): + return f'{chr(self.ty.value)}.{self.id}.{self.hash}' + + +@dataclass +class EntityCache: + hash_map: dict = field(default_factory=dict) # id -> (hash, ty) + self_id: int = None + self_bot: bool = False + + def set_self_user(self, id, bot): + self.self_id = id + self.self_bot = bot + + def get(self, id): + value = self.hash_map.get(id) + return PackedChat(ty=value[1], id=id, hash=value[0]) if value else None + + def extend(self, users, chats): + # See https://core.telegram.org/api/min for "issues" with "min constructors". + self.hash_map.update( + (u.id, ( + u.access_hash, + EntityType.BOT if u.bot else EntityType.USER, + )) + for u in users + if getattr(u, 'access_hash', None) and not u.min + ) + self.hash_map.update( + (c.id, ( + c.access_hash, + EntityType.MEGAGROUP if c.megagroup else ( + EntityType.GIGAGROUP if getattr(c, 'gigagroup', None) else EntityType.CHANNEL + ), + )) + for c in chats + if getattr(c, 'access_hash', None) and not getattr(c, 'min', None) + ) diff --git a/telethon/_updates/messagebox.py b/telethon/_updates/messagebox.py new file mode 100644 index 00000000..555451ad --- /dev/null +++ b/telethon/_updates/messagebox.py @@ -0,0 +1,565 @@ +""" +This module deals with correct handling of updates, including gaps, and knowing when the code +should "get difference" (the set of updates that the client should know by now minus the set +of updates that it actually knows). + +Each chat has its own [`Entry`] in the [`MessageBox`] (this `struct` is the "entry point"). +At any given time, the message box may be either getting difference for them (entry is in +[`MessageBox::getting_diff_for`]) or not. If not getting difference, a possible gap may be +found for the updates (entry is in [`MessageBox::possible_gaps`]). Otherwise, the entry is +on its happy path. + +Gaps are cleared when they are either resolved on their own (by waiting for a short time) +or because we got the difference for the corresponding entry. + +While there are entries for which their difference must be fetched, +[`MessageBox::check_deadlines`] will always return [`Instant::now`], since "now" is the time +to get the difference. +""" +import asyncio +from dataclasses import dataclass, field +from .._sessions.types import SessionState, ChannelState + + +# Telegram sends `seq` equal to `0` when "it doesn't matter", so we use that value too. +NO_SEQ = 0 + +# See https://core.telegram.org/method/updates.getChannelDifference. +BOT_CHANNEL_DIFF_LIMIT = 100000 +USER_CHANNEL_DIFF_LIMIT = 100 + +# > It may be useful to wait up to 0.5 seconds +POSSIBLE_GAP_TIMEOUT = 0.5 + +# After how long without updates the client will "timeout". +# +# When this timeout occurs, the client will attempt to fetch updates by itself, ignoring all the +# updates that arrive in the meantime. After all updates are fetched when this happens, the +# client will resume normal operation, and the timeout will reset. +# +# Documentation recommends 15 minutes without updates (https://core.telegram.org/api/updates). +NO_UPDATES_TIMEOUT = 15 * 60 + +# Entry "enum". +# Account-wide `pts` includes private conversations (one-to-one) and small group chats. +ENTRY_ACCOUNT = object() +# Account-wide `qts` includes only "secret" one-to-one chats. +ENTRY_SECRET = object() +# Integers will be Channel-specific `pts`, and includes "megagroup", "broadcast" and "supergroup" channels. + + +def next_updates_deadline(): + return asyncio.get_running_loop().time() + NO_UPDATES_TIMEOUT + + +class GapError(ValueError): + pass + + +# Represents the information needed to correctly handle a specific `tl::enums::Update`. +@dataclass +class PtsInfo: + pts: int + pts_count: int + entry: object + + @classmethod + def from_update(cls, update): + pts = getattr(update, 'pts', None) + if pts: + pts_count = getattr(update, 'pts_count', None) or 0 + entry = getattr(update, 'channel_id', None) or ENTRY_ACCOUNT + return cls(pts=pts, pts_count=pts_count, entry=entry) + + qts = getattr(update, 'qts', None) + if qts: + pts_count = 1 if isinstance(update, _tl.UpdateNewEncryptedMessage) else 0 + return cls(pts=qts, pts_count=pts_count, entry=ENTRY_SECRET) + + return None + + +# The state of a particular entry in the message box. +@dataclass +class State: + # Current local persistent timestamp. + pts: int + + # Next instant when we would get the update difference if no updates arrived before then. + deadline: float + + +# > ### Recovering gaps +# > […] Manually obtaining updates is also required in the following situations: +# > • Loss of sync: a gap was found in `seq` / `pts` / `qts` (as described above). +# > It may be useful to wait up to 0.5 seconds in this situation and abort the sync in case a new update +# > arrives, that fills the gap. +# +# This is really easy to trigger by spamming messages in a channel (with as little as 3 members works), because +# the updates produced by the RPC request take a while to arrive (whereas the read update comes faster alone). +@dataclass +class PossibleGap: + deadline: float + # Pending updates (those with a larger PTS, producing the gap which may later be filled). + updates: list # of updates + + +# Represents a "message box" (event `pts` for a specific entry). +# +# See https://core.telegram.org/api/updates#message-related-event-sequences. +@dataclass +class MessageBox: + # Map each entry to their current state. + map: dict = field(default_factory=dict) # entry -> state + + # Additional fields beyond PTS needed by `ENTRY_ACCOUNT`. + date: int = 1 + seq: int = 0 + + # Holds the entry with the closest deadline (optimization to avoid recalculating the minimum deadline). + next_deadline: object = None # entry + + # Which entries have a gap and may soon trigger a need to get difference. + # + # If a gap is found, stores the required information to resolve it (when should it timeout and what updates + # should be held in case the gap is resolved on its own). + # + # Not stored directly in `map` as an optimization (else we would need another way of knowing which entries have + # a gap in them). + possible_gaps: dict = field(default_factory=dict) # entry -> possiblegap + + # For which entries are we currently getting difference. + getting_diff_for: set = field(default_factory=set) # entry + + # Temporarily stores which entries should have their update deadline reset. + # Stored in the message box in order to reuse the allocation. + reset_deadlines_for: set = field(default_factory=set) # entry + + # region Creation, querying, and setting base state. + + @classmethod + def load(cls, session_state, channel_states): + """ + Create a [`MessageBox`] from a previously known update state. + """ + deadline = next_updates_deadline() + return cls( + map={ + ENTRY_ACCOUNT: State(pts=session_state.pts, deadline=deadline), + ENTRY_SECRET: State(pts=session_state.qts, deadline=deadline), + **{s.channel_id: s.pts for s in channel_states} + }, + date=session_state.date, + seq=session_state.seq, + next_deadline=ENTRY_ACCOUNT, + ) + + @classmethod + def session_state(self): + """ + Return the current state in a format that sessions understand. + + This should be used for persisting the state. + """ + return SessionState( + user_id=0, + dc_id=0, + bot=False, + pts=self.map.get(ENTRY_ACCOUNT, 0), + qts=self.map.get(ENTRY_SECRET, 0), + date=self.date, + seq=self.seq, + takeout_id=None, + ), [ChannelState(channel_id=id, pts=pts) for id, pts in self.map.items() if isinstance(id, int)] + + def is_empty(self) -> bool: + """ + Return true if the message box is empty and has no state yet. + """ + return self.map.get(ENTRY_ACCOUNT, NO_SEQ) == NO_SEQ + + def check_deadlines(self): + """ + Return the next deadline when receiving updates should timeout. + + If a deadline expired, the corresponding entries will be marked as needing to get its difference. + While there are entries pending of getting their difference, this method returns the current instant. + """ + now = asyncio.get_running_loop().time() + + if self.getting_diff_for: + return now + + deadline = next_updates_deadline() + + # Most of the time there will be zero or one gap in flight so finding the minimum is cheap. + if self.possible_gaps: + deadline = min(deadline, *self.possible_gaps.values()) + elif self.next_deadline in self.map: + deadline = min(deadline, self.map[self.next_deadline]) + + if now > deadline: + # Check all expired entries and add them to the list that needs getting difference. + self.getting_diff_for.update(entry for entry, gap in self.possible_gaps.items() if now > gap.deadline) + self.getting_diff_for.update(entry for entry, state in self.map.items() if now > state.deadline) + + # When extending `getting_diff_for`, it's important to have the moral equivalent of + # `begin_get_diff` (that is, clear possible gaps if we're now getting difference). + for entry in self.getting_diff_for: + self.possible_gaps.pop(entry, None) + + return deadline + + # Reset the deadline for the periods without updates for a given entry. + # + # It also updates the next deadline time to reflect the new closest deadline. + def reset_deadline(self, entry, deadline): + if entry in self.map: + self.map[entry].deadline = deadline + # TODO figure out why not in map may happen + + if self.next_deadline == entry: + # If the updated deadline was the closest one, recalculate the new minimum. + self.next_deadline = min(self.map.items(), key=lambda entry_state: entry_state[1].deadline)[0] + elif deadline < self.map.get(self.next_deadline, 0): + # If the updated deadline is smaller than the next deadline, change the next deadline to be the new one. + self.next_deadline = entry + # else an unrelated deadline was updated, so the closest one remains unchanged. + + # Convenience to reset a channel's deadline, with optional timeout. + def reset_channel_deadline(self, channel_id, timeout): + self.reset_deadlines(channel_id, asyncio.get_running_loop().time() + (timeout or NO_UPDATES_TIMEOUT)) + + # Reset all the deadlines in `reset_deadlines_for` and then empty the set. + def apply_deadlines_reset(self): + next_deadline = next_updates_deadline() + + reset_deadlines_for = self.reset_deadlines_for + self.reset_deadlines_for = set() # "move" the set to avoid self.reset_deadline() from touching it during iter + + for entry in reset_deadlines_for: + self.reset_deadline(entry, next_deadline) + + reset_deadlines_for.clear() # reuse allocation, the other empty set was a temporary dummy value + self.reset_deadlines_for = reset_deadlines_for + + # Sets the update state. + # + # Should be called right after login if [`MessageBox::new`] was used, otherwise undesirable + # updates will be fetched. + def set_state(self, state): + deadline = next_updates_deadline() + self.map[ENTRY_ACCOUNT] = State(pts=state.pts, deadline=deadline) + self.map[ENTRY_SECRET] = State(pts=state.qts, deadline=deadline) + self.date = state.date + self.seq = state.seq + + # Like [`MessageBox::set_state`], but for channels. Useful when getting dialogs. + # + # The update state will only be updated if no entry was known previously. + def try_set_channel_state(self, id, pts): + if id not in self.map: + self.map[id] = State(pts=pts, deadline=next_updates_deadline()) + + # Begin getting difference for the given entry. + # + # Clears any previous gaps. + def begin_get_diff(self, entry): + self.getting_diff_for.add(entry) + self.possible_gaps.pop(entry, None) + + # Finish getting difference for the given entry. + # + # It also resets the deadline. + def end_get_diff(self, entry): + self.getting_diff_for.pop(entry, None) + self.reset_deadline(entry, next_updates_deadline()) + assert entry not in self.possible_gaps, "gaps shouldn't be created while getting difference" + + # endregion Creation, querying, and setting base state. + + # region "Normal" updates flow (processing and detection of gaps). + + # Process an update and return what should be done with it. + # + # Updates corresponding to entries for which their difference is currently being fetched + # will be ignored. While according to the [updates' documentation]: + # + # > Implementations [have] to postpone updates received via the socket while + # > filling gaps in the event and `Update` sequences, as well as avoid filling + # > gaps in the same sequence. + # + # In practice, these updates should have also been retrieved through getting difference. + # + # [updates documentation] https://core.telegram.org/api/updates + def process_updates( + self, + updates, + chat_hashes, + result, # out list of updates; returns list of user, chat, or raise if gap + ): + # XXX adapt updates and chat hashes into updatescombined, raise gap on too long + date = updates.date + seq_start = updates.seq_start + seq = updates.seq + updates = updates.updates + users = updates.users + chats = updates.chats + + # > For all the other [not `updates` or `updatesCombined`] `Updates` type constructors + # > there is no need to check `seq` or change a local state. + if updates.seq_start != NO_SEQ: + if self.seq + 1 > updates.seq_start: + # Skipping updates that were already handled + return (updates.users, updates.chats) + elif self.seq + 1 < updates.seq_start: + # Gap detected + self.begin_get_diff(ENTRY_ACCOUNT) + raise GapError + # else apply + + self.date = updates.date + if updates.seq != NO_SEQ: + self.seq = updates.seq + + result.extend(filter(None, (self.apply_pts_info(u, reset_deadline=True) for u in updates.updates))) + + self.apply_deadlines_reset() + + def _sort_gaps(update): + pts = PtsInfo.from_update(u) + return pts.pts - pts.pts_count if pts else 0 + + if self.possible_gaps: + # For each update in possible gaps, see if the gap has been resolved already. + for key in list(self.possible_gaps.keys()): + self.possible_gaps[key].updates.sort(key=_sort_gaps) + + for _ in range(len(self.possible_gaps[key].updates)): + update = self.possible_gaps[key].updates.pop(0) + + # If this fails to apply, it will get re-inserted at the end. + # All should fail, so the order will be preserved (it would've cycled once). + update = self.apply_pts_info(update, reset_deadline=False) + if update: + result.append(update) + + # Clear now-empty gaps. + self.possible_gaps = {entry: gap for entry, gap in self.possible_gaps if gap.updates} + + return (updates.users, updates.chats) + + # Tries to apply the input update if its `PtsInfo` follows the correct order. + # + # If the update can be applied, it is returned; otherwise, the update is stored in a + # possible gap (unless it was already handled or would be handled through getting + # difference) and `None` is returned. + def apply_pts_info( + self, + update, + *, + reset_deadline, + ): + pts = PtsInfo.from_update(update) + if not pts: + # No pts means that the update can be applied in any order. + return update + + # As soon as we receive an update of any form related to messages (has `PtsInfo`), + # the "no updates" period for that entry is reset. + # + # Build the `HashSet` to avoid calling `reset_deadline` more than once for the same entry. + if reset_deadline: + self.reset_deadlines_for.insert(pts.entry) + + if pts.entry in self.getting_diff_for: + # Note: early returning here also prevents gap from being inserted (which they should + # not be while getting difference). + return None + + if pts.entry in self.map: + local_pts = self.map[pts.entry].pts + if local_pts + pts.pts_count > pts.pts: + # Ignore + return None + elif local_pts + pts.pts_count < pts.pts: + # Possible gap + # TODO store chats too? + if pts.entry not in self.possible_gaps: + self.possible_gaps[pts.entry] = PossibleGap( + deadline=asyncio.get_running_loop().time() + POSSIBLE_GAP_TIMEOUT, + updates=[] + ) + + self.possible_gaps[pts.entry].updates.append(update) + return None + else: + # Apply + pass + else: + # No previous `pts` known, and because this update has to be "right" (it's the first one) our + # `local_pts` must be one less. + local_pts = pts.pts - 1 + + # For example, when we're in a channel, we immediately receive: + # * ReadChannelInbox (pts = X) + # * NewChannelMessage (pts = X, pts_count = 1) + # + # Notice how both `pts` are the same. If we stored the one from the first, then the second one would + # be considered "already handled" and ignored, which is not desirable. Instead, advance local `pts` + # by `pts_count` (which is 0 for updates not directly related to messages, like reading inbox). + if pts.entry in self.map: + self.map[pts.entry].pts = local_pts + pts.pts_count + else: + self.map[pts.entry] = State(pts=local_pts + pts.pts_count, deadline=next_updates_deadline()) + + return update + + # endregion "Normal" updates flow (processing and detection of gaps). + + # region Getting and applying account difference. + + # Return the request that needs to be made to get the difference, if any. + def get_difference(self): + entry = ENTRY_ACCOUNT + if entry in self.getting_diff_for: + if entry in self.map: + return _tl.fn.updates.GetDifference( + pts=state.pts, + pts_total_limit=None, + date=self.date, + qts=self.map[ENTRY_SECRET].pts, + ) + else: + # TODO investigate when/why/if this can happen + self.end_get_diff(entry) + + return None + + # Similar to [`MessageBox::process_updates`], but using the result from getting difference. + def apply_difference( + self, + diff, + chat_hashes, + ): + if isinstance(diff, _tl.updates.DifferenceEmpty): + self.date = diff.date + self.seq = diff.seq + self.end_get_diff(ENTRY_ACCOUNT) + return [], [], [] + elif isinstance(diff, _tl.updates.Difference): + self.end_get_diff(ENTRY_ACCOUNT) + chat_hashes.extend(diff.users, diff.chats) + return self.apply_difference_type(diff) + elif isinstance(diff, _tl.updates.DifferenceSlice): + chat_hashes.extend(diff.users, diff.chats) + return self.apply_difference_type(diff) + elif isinstance(diff, _tl.updates.DifferenceTooLong): + # TODO when are deadlines reset if we update the map?? + self.map[ENTRY_ACCOUNT].pts = diff.pts + self.end_get_diff(ENTRY_ACCOUNT) + return [], [], [] + + def apply_difference_type( + self, + diff, + ): + state = getattr(diff, 'intermediate_state', None) or diff.state + self.map[ENTRY_ACCOUNT].pts = state.pts + self.map[ENTRY_SECRET].pts = state.qts + self.date = state.date + self.seq = state.seq + + for u in diff.updates: + if isinstance(u, _tl.UpdateChannelTooLong): + self.begin_get_diff(u.channel_id) + + updates.extend(_tl.UpdateNewMessage( + message=m, + pts=NO_SEQ, + pts_count=NO_SEQ, + ) for m in diff.new_messages) + updates.extend(_tl.UpdateNewEncryptedMessage( + message=m, + qts=NO_SEQ, + ) for m in diff.new_encrypted_messages) + + return diff.updates, diff.users, diff.chats + + # endregion Getting and applying account difference. + + # region Getting and applying channel difference. + + # Return the request that needs to be made to get a channel's difference, if any. + def get_channel_difference( + self, + chat_hashes, + ): + entry = next((id for id in self.getting_diff_for if isinstance(id, int)), None) + if not entry: + return None + + packed = chat_hashes.get(entry) + if not packed: + # Cannot get channel difference as we're missing its hash + self.end_get_diff(entry) + # Remove the outdated `pts` entry from the map so that the next update can correct + # it. Otherwise, it will spam that the access hash is missing. + self.map.pop(entry, None) + return None + + state = self.map.get(entry) + if not state: + # TODO investigate when/why/if this can happen + # Cannot get channel difference as we're missing its pts + self.end_get_diff(entry) + return None + + return _tl.fn.updates.GetChannelDifference( + force=False, + channel=channel, + filter=_tl.ChannelMessagesFilterEmpty(), + pts=state.pts, + limit=BOT_CHANNEL_DIFF_LIMIT if chat_hashes.is_self_bot() else USER_CHANNEL_DIFF_LIMIT + ) + + # Similar to [`MessageBox::process_updates`], but using the result from getting difference. + def apply_channel_difference( + self, + request, + diff, + chat_hashes, + ): + entry = request.channel.channel_id + self.possible_gaps.remove(entry) + + if isinstance(diff, _tl.updates.ChannelDifferenceEmpty): + assert diff.final + self.end_get_diff(entry) + self.map[entry].pts = diff.pts + return [], [], [] + elif isinstance(diff, _tl.updates.ChannelDifferenceTooLong): + assert diff.final + self.map[entry].pts = diff.dialog.pts + chat_hashes.extend(diff.users, diff.chats) + self.reset_channel_deadline(channel_id, diff.timeout) + # This `diff` has the "latest messages and corresponding chats", but it would + # be strange to give the user only partial changes of these when they would + # expect all updates to be fetched. Instead, nothing is returned. + return [], [], [] + elif isinstance(diff, _tl.updates.ChannelDifference): + if diff.final: + self.end_get_diff(entry) + + self.map[entry].pts = pts + updates.extend(_tl.UpdateNewMessage( + message=m, + pts=NO_SEQ, + pts_count=NO_SEQ, + ) for m in diff.new_messages) + chat_hashes.extend(diff.users, diff.chats); + self.reset_channel_deadline(channel_id, timeout) + + (diff.updates, diff.users, diff.chats) + + # endregion Getting and applying channel difference. From 01291922c97ae0d78a7be0ab3b8fcc2f573e03c1 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 22 Jan 2022 13:27:00 +0100 Subject: [PATCH 151/256] Proper usage of messagebox in update handling loop --- telethon/_client/updates.py | 43 ++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index 2460c561..b0d632b5 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -7,6 +7,7 @@ import time import traceback import typing import logging +from collections import deque from ..errors._rpcbase import RpcError from .._events.common import EventBuilder, EventCommon @@ -82,6 +83,42 @@ async def catch_up(self: 'TelegramClient'): pass async def _update_loop(self: 'TelegramClient'): - while self.is_connected(): - updates = await self._updates_queue.get() - updates, users, chats = self._message_box.process_updates(updates, self._entity_cache) + try: + updates_to_dispatch = deque() + while self.is_connected(): + if updates_to_dispatch: + # TODO dispatch + updates_to_dispatch.popleft() + continue + + get_diff = self._message_box.get_difference() + if get_diff: + self._log[__name__].info('Getting difference for account updates') + diff = await self(get_diff) + updates, users, chats = self._message_box.apply_difference(diff, self._entity_cache) + updates_to_dispatch.extend(updates) + continue + + get_diff = self._message_box.get_channel_difference(self._entity_cache) + if get_diff: + self._log[__name__].info('Getting difference for channel updates') + diff = await self(get_diff) + updates, users, chats = self._message_box.apply_channel_difference(diff, self._entity_cache) + updates_to_dispatch.extend(updates) + continue + + deadline = self._message_box.check_deadlines() + try: + updates = await asyncio.wait_for( + self._updates_queue.get(), + deadline - asyncio.get_running_loop().time() + ) + except asyncio.TimeoutError: + self._log[__name__].info('Timeout waiting for updates expired') + continue + + processed = [] + users, chats = self._message_box.process_updates(updates, self._entity_cache, processed) + updates_to_dispatch.extend(processed) + except Exception: + self._log[__name__].exception('Fatal error handling updates (this is a bug in Telethon, please report it)') From 259fccaaa90e73b64d90d86993f4bf54e719be1b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 22 Jan 2022 13:27:14 +0100 Subject: [PATCH 152/256] Fix messagebox porting errors --- telethon/_updates/messagebox.py | 66 +++++++++++++++++---------------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/telethon/_updates/messagebox.py b/telethon/_updates/messagebox.py index 555451ad..3035ae0f 100644 --- a/telethon/_updates/messagebox.py +++ b/telethon/_updates/messagebox.py @@ -19,6 +19,7 @@ to get the difference. import asyncio from dataclasses import dataclass, field from .._sessions.types import SessionState, ChannelState +from .. import _tl # Telegram sends `seq` equal to `0` when "it doesn't matter", so we use that value too. @@ -194,7 +195,7 @@ class MessageBox: # Most of the time there will be zero or one gap in flight so finding the minimum is cheap. if self.possible_gaps: - deadline = min(deadline, *self.possible_gaps.values()) + deadline = min(deadline, *(gap.deadline for gap in self.possible_gaps.values())) elif self.next_deadline in self.map: deadline = min(deadline, self.map[self.next_deadline]) @@ -272,7 +273,10 @@ class MessageBox: # # It also resets the deadline. def end_get_diff(self, entry): - self.getting_diff_for.pop(entry, None) + try: + self.getting_diff_for.remove(entry) + except KeyError: + pass self.reset_deadline(entry, next_updates_deadline()) assert entry not in self.possible_gaps, "gaps shouldn't be created while getting difference" @@ -298,36 +302,39 @@ class MessageBox: chat_hashes, result, # out list of updates; returns list of user, chat, or raise if gap ): - # XXX adapt updates and chat hashes into updatescombined, raise gap on too long - date = updates.date - seq_start = updates.seq_start - seq = updates.seq - updates = updates.updates - users = updates.users - chats = updates.chats + date = getattr(updates, 'date', None) + if date is None: + # updatesTooLong is the only one with no date (we treat it as a gap) + raise GapError + + seq = getattr(updates, 'seq', None) or NO_SEQ + seq_start = getattr(updates, 'seq_start', None) or seq + users = getattr(updates, 'users') or [] + chats = getattr(updates, 'chats') or [] + updates = getattr(updates, 'updates', None) or [updates] # > For all the other [not `updates` or `updatesCombined`] `Updates` type constructors # > there is no need to check `seq` or change a local state. - if updates.seq_start != NO_SEQ: - if self.seq + 1 > updates.seq_start: + if seq_start != NO_SEQ: + if self.seq + 1 > seq_start: # Skipping updates that were already handled - return (updates.users, updates.chats) - elif self.seq + 1 < updates.seq_start: + return (users, chats) + elif self.seq + 1 < seq_start: # Gap detected self.begin_get_diff(ENTRY_ACCOUNT) raise GapError # else apply - self.date = updates.date - if updates.seq != NO_SEQ: - self.seq = updates.seq + self.date = date + if seq != NO_SEQ: + self.seq = seq - result.extend(filter(None, (self.apply_pts_info(u, reset_deadline=True) for u in updates.updates))) + result.extend(filter(None, (self.apply_pts_info(u, reset_deadline=True) for u in updates))) self.apply_deadlines_reset() def _sort_gaps(update): - pts = PtsInfo.from_update(u) + pts = PtsInfo.from_update(update) return pts.pts - pts.pts_count if pts else 0 if self.possible_gaps: @@ -345,9 +352,9 @@ class MessageBox: result.append(update) # Clear now-empty gaps. - self.possible_gaps = {entry: gap for entry, gap in self.possible_gaps if gap.updates} + self.possible_gaps = {entry: gap for entry, gap in self.possible_gaps.items() if gap.updates} - return (updates.users, updates.chats) + return (users, chats) # Tries to apply the input update if its `PtsInfo` follows the correct order. # @@ -370,7 +377,7 @@ class MessageBox: # # Build the `HashSet` to avoid calling `reset_deadline` more than once for the same entry. if reset_deadline: - self.reset_deadlines_for.insert(pts.entry) + self.reset_deadlines_for.add(pts.entry) if pts.entry in self.getting_diff_for: # Note: early returning here also prevents gap from being inserted (which they should @@ -425,10 +432,10 @@ class MessageBox: if entry in self.getting_diff_for: if entry in self.map: return _tl.fn.updates.GetDifference( - pts=state.pts, + pts=self.map[ENTRY_ACCOUNT].pts, pts_total_limit=None, date=self.date, - qts=self.map[ENTRY_SECRET].pts, + qts=self.map[ENTRY_SECRET].pts if ENTRY_SECRET in self.map else NO_SEQ, ) else: # TODO investigate when/why/if this can happen @@ -465,26 +472,23 @@ class MessageBox: diff, ): state = getattr(diff, 'intermediate_state', None) or diff.state - self.map[ENTRY_ACCOUNT].pts = state.pts - self.map[ENTRY_SECRET].pts = state.qts - self.date = state.date - self.seq = state.seq + self.set_state(state) - for u in diff.updates: + for u in diff.other_updates: if isinstance(u, _tl.UpdateChannelTooLong): self.begin_get_diff(u.channel_id) - updates.extend(_tl.UpdateNewMessage( + diff.other_updates.extend(_tl.UpdateNewMessage( message=m, pts=NO_SEQ, pts_count=NO_SEQ, ) for m in diff.new_messages) - updates.extend(_tl.UpdateNewEncryptedMessage( + diff.other_updates.extend(_tl.UpdateNewEncryptedMessage( message=m, qts=NO_SEQ, ) for m in diff.new_encrypted_messages) - return diff.updates, diff.users, diff.chats + return diff.other_updates, diff.users, diff.chats # endregion Getting and applying account difference. From 0d597d1003a32b8df0882a6659cb8aacd354eeb0 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 23 Jan 2022 12:23:56 +0100 Subject: [PATCH 153/256] Remove GitHub workflow It's currently broken. --- .github/workflows/python.yml | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 .github/workflows/python.yml diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml deleted file mode 100644 index f3fd3106..00000000 --- a/.github/workflows/python.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Python Library - -on: [push, pull_request] - -jobs: - build: - - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] - steps: - - uses: actions/checkout@v1 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python-version }} - - name: Set up env - run: | - python -m pip install --upgrade pip - pip install tox - - name: Lint with flake8 - run: | - tox -e flake - - name: Test with pytest - run: | - # use "py", which is the default python version - tox -e py From de2cd1f2cfd568e3829bacffb9191d12f3121397 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 23 Jan 2022 12:34:16 +0100 Subject: [PATCH 154/256] Fix constructing PtsInfo for channels --- telethon/_updates/messagebox.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/telethon/_updates/messagebox.py b/telethon/_updates/messagebox.py index 3035ae0f..542fc629 100644 --- a/telethon/_updates/messagebox.py +++ b/telethon/_updates/messagebox.py @@ -69,7 +69,10 @@ class PtsInfo: pts = getattr(update, 'pts', None) if pts: pts_count = getattr(update, 'pts_count', None) or 0 - entry = getattr(update, 'channel_id', None) or ENTRY_ACCOUNT + try: + entry = update.message.peer_id.channel_id + except AttributeError: + entry = getattr(update, 'channel_id', None) or ENTRY_ACCOUNT return cls(pts=pts, pts_count=pts_count, entry=entry) qts = getattr(update, 'qts', None) From 1f40372235e0ffbddff7832e48e46252b2efb1d6 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 23 Jan 2022 12:43:41 +0100 Subject: [PATCH 155/256] Fix update handling for channels --- telethon/_client/updates.py | 5 ++++- telethon/_updates/messagebox.py | 20 ++++++++++---------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index b0d632b5..e3e8fd78 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -96,6 +96,7 @@ async def _update_loop(self: 'TelegramClient'): self._log[__name__].info('Getting difference for account updates') diff = await self(get_diff) updates, users, chats = self._message_box.apply_difference(diff, self._entity_cache) + self._entity_cache.extend(users, chats) updates_to_dispatch.extend(updates) continue @@ -103,7 +104,8 @@ async def _update_loop(self: 'TelegramClient'): if get_diff: self._log[__name__].info('Getting difference for channel updates') diff = await self(get_diff) - updates, users, chats = self._message_box.apply_channel_difference(diff, self._entity_cache) + updates, users, chats = self._message_box.apply_channel_difference(get_diff, diff, self._entity_cache) + self._entity_cache.extend(users, chats) updates_to_dispatch.extend(updates) continue @@ -119,6 +121,7 @@ async def _update_loop(self: 'TelegramClient'): processed = [] users, chats = self._message_box.process_updates(updates, self._entity_cache, processed) + self._entity_cache.extend(users, chats) updates_to_dispatch.extend(processed) except Exception: self._log[__name__].exception('Fatal error handling updates (this is a bug in Telethon, please report it)') diff --git a/telethon/_updates/messagebox.py b/telethon/_updates/messagebox.py index 542fc629..235247f3 100644 --- a/telethon/_updates/messagebox.py +++ b/telethon/_updates/messagebox.py @@ -232,7 +232,7 @@ class MessageBox: # Convenience to reset a channel's deadline, with optional timeout. def reset_channel_deadline(self, channel_id, timeout): - self.reset_deadlines(channel_id, asyncio.get_running_loop().time() + (timeout or NO_UPDATES_TIMEOUT)) + self.reset_deadline(channel_id, asyncio.get_running_loop().time() + (timeout or NO_UPDATES_TIMEOUT)) # Reset all the deadlines in `reset_deadlines_for` and then empty the set. def apply_deadlines_reset(self): @@ -524,10 +524,10 @@ class MessageBox: return _tl.fn.updates.GetChannelDifference( force=False, - channel=channel, + channel=packed.try_to_input_channel(), filter=_tl.ChannelMessagesFilterEmpty(), pts=state.pts, - limit=BOT_CHANNEL_DIFF_LIMIT if chat_hashes.is_self_bot() else USER_CHANNEL_DIFF_LIMIT + limit=BOT_CHANNEL_DIFF_LIMIT if chat_hashes.self_bot else USER_CHANNEL_DIFF_LIMIT ) # Similar to [`MessageBox::process_updates`], but using the result from getting difference. @@ -538,7 +538,7 @@ class MessageBox: chat_hashes, ): entry = request.channel.channel_id - self.possible_gaps.remove(entry) + self.possible_gaps.pop(entry, None) if isinstance(diff, _tl.updates.ChannelDifferenceEmpty): assert diff.final @@ -549,7 +549,7 @@ class MessageBox: assert diff.final self.map[entry].pts = diff.dialog.pts chat_hashes.extend(diff.users, diff.chats) - self.reset_channel_deadline(channel_id, diff.timeout) + self.reset_channel_deadline(entry, diff.timeout) # This `diff` has the "latest messages and corresponding chats", but it would # be strange to give the user only partial changes of these when they would # expect all updates to be fetched. Instead, nothing is returned. @@ -558,15 +558,15 @@ class MessageBox: if diff.final: self.end_get_diff(entry) - self.map[entry].pts = pts - updates.extend(_tl.UpdateNewMessage( + self.map[entry].pts = diff.pts + diff.other_updates.extend(_tl.UpdateNewMessage( message=m, pts=NO_SEQ, pts_count=NO_SEQ, ) for m in diff.new_messages) - chat_hashes.extend(diff.users, diff.chats); - self.reset_channel_deadline(channel_id, timeout) + chat_hashes.extend(diff.users, diff.chats) + self.reset_channel_deadline(entry, None) - (diff.updates, diff.users, diff.chats) + return diff.other_updates, diff.users, diff.chats # endregion Getting and applying channel difference. From f1a517dee6e1b4592fa13f4de7f5d731f9dfed07 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 23 Jan 2022 13:20:35 +0100 Subject: [PATCH 156/256] Process self-produced updates like any other --- readthedocs/misc/v2-migration-guide.rst | 2 ++ telethon/_network/mtprotosender.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 669ed8a8..dc751b1a 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -771,3 +771,5 @@ sequential_updates is gone connection type is gone raise_last_call_error is now the default rather than ValueError + +self-produced updates like getmessage now also trigger a handler diff --git a/telethon/_network/mtprotosender.py b/telethon/_network/mtprotosender.py index fa58240f..177813cd 100644 --- a/telethon/_network/mtprotosender.py +++ b/telethon/_network/mtprotosender.py @@ -603,6 +603,7 @@ class MTProtoSender: if not state.future.cancelled(): state.future.set_exception(e) else: + self._store_own_updates(result) if not state.future.cancelled(): state.future.set_result(result) @@ -646,6 +647,20 @@ class MTProtoSender: ) self._last_update_warn = now + def _store_own_updates(self, obj, *, _update_ids=frozenset(( + _tl.UpdateShortMessage.CONSTRUCTOR_ID, + _tl.UpdateShortChatMessage.CONSTRUCTOR_ID, + _tl.UpdateShort.CONSTRUCTOR_ID, + _tl.UpdatesCombined.CONSTRUCTOR_ID, + _tl.Updates.CONSTRUCTOR_ID, + _tl.UpdateShortSentMessage.CONSTRUCTOR_ID, + ))): + try: + if obj.CONSTRUCTOR_ID in _update_ids: + self._updates_queue.put_nowait(obj) + except AttributeError: + pass + async def _handle_pong(self, message): """ Handles pong results, which don't come inside a ``rpc_result`` From 015acf20c6b8e7ce06b17e5f0b017b410b1f0751 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 23 Jan 2022 13:26:53 +0100 Subject: [PATCH 157/256] Handle TypeNotFoundError during gzip packed msgs --- telethon/_network/mtprotosender.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/telethon/_network/mtprotosender.py b/telethon/_network/mtprotosender.py index 177813cd..2c9c1c59 100644 --- a/telethon/_network/mtprotosender.py +++ b/telethon/_network/mtprotosender.py @@ -625,8 +625,16 @@ class MTProtoSender: """ self._log.debug('Handling gzipped data') with BinaryReader(message.obj.data) as reader: - message.obj = reader.tgread_object() - await self._process_message(message) + try: + message.obj = reader.tgread_object() + except TypeNotFoundError as e: + # Received object which we don't know how to deserialize. + # This is somewhat expected while receiving updates, which + # will eventually trigger a gap error to recover from. + self._log.info('Type %08x not found, remaining data %r', + e.invalid_constructor_id, e.remaining) + else: + await self._process_message(message) async def _handle_update(self, message): try: From f547a00da3e4e04f56ca48fec9591d200c9522cf Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 23 Jan 2022 19:46:37 +0100 Subject: [PATCH 158/256] Persist session state and usage fixes Catching up is now an option when creating the client. --- telethon/_client/telegrambaseclient.py | 13 +++++++++ telethon/_sessions/sqlite.py | 2 +- telethon/_updates/entitycache.py | 3 ++ telethon/_updates/messagebox.py | 40 +++++++++++--------------- 4 files changed, 33 insertions(+), 25 deletions(-) diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index b6872dc8..5c32d086 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -91,6 +91,7 @@ def init( request_retries: int = 4, flood_sleep_threshold: int = 60, # Update handling. + catch_up: bool = False, receive_updates: bool = True, max_queued_updates: int = 100, ): @@ -142,6 +143,7 @@ def init( self._parse_mode = markdown # Update handling. + self._catch_up = catch_up self._no_updates = not receive_updates self._updates_queue = asyncio.Queue(maxsize=max_queued_updates) self._updates_handle = None @@ -232,6 +234,8 @@ async def connect(self: 'TelegramClient') -> None: ) else: try_fetch_user = self._session_state.user_id == 0 + if self._catch_up: + self._message_box.load(self._session_state, await self._session.get_all_channel_states()) dc = all_dcs.get(self._session_state.dc_id) if dc is None: @@ -358,6 +362,15 @@ async def _disconnect(self: 'TelegramClient'): except asyncio.CancelledError: pass + await self._session.insert_entities(self._entity_cache.get_all_entities()) + + session_state, channel_states = self._message_box.session_state() + for channel_id, pts in channel_states.items(): + await self._session.insert_channel_state(channel_id, pts) + + await self._replace_session_state(**session_state) + + async def _switch_dc(self: 'TelegramClient', new_dc): """ Permanently switches the current connection to the new data center. diff --git a/telethon/_sessions/sqlite.py b/telethon/_sessions/sqlite.py index 2ea419be..b41975fb 100644 --- a/telethon/_sessions/sqlite.py +++ b/telethon/_sessions/sqlite.py @@ -245,7 +245,7 @@ class SQLiteSession(Session): try: c.executemany( 'insert or replace into entity values (?,?,?)', - [(e.id, e.access_hash, e.ty) for e in entities] + [(e.id, e.access_hash, e.ty.value) for e in entities] ) finally: c.close() diff --git a/telethon/_updates/entitycache.py b/telethon/_updates/entitycache.py index 176d2013..ce89eb4f 100644 --- a/telethon/_updates/entitycache.py +++ b/telethon/_updates/entitycache.py @@ -95,3 +95,6 @@ class EntityCache: for c in chats if getattr(c, 'access_hash', None) and not getattr(c, 'min', None) ) + + def get_all_entities(self): + return [Entity(ty, id, hash) for id, (hash, ty) in self.hash_map.items()] diff --git a/telethon/_updates/messagebox.py b/telethon/_updates/messagebox.py index 235247f3..232ef446 100644 --- a/telethon/_updates/messagebox.py +++ b/telethon/_updates/messagebox.py @@ -141,46 +141,38 @@ class MessageBox: # region Creation, querying, and setting base state. - @classmethod - def load(cls, session_state, channel_states): + def load(self, session_state, channel_states): """ Create a [`MessageBox`] from a previously known update state. """ deadline = next_updates_deadline() - return cls( - map={ - ENTRY_ACCOUNT: State(pts=session_state.pts, deadline=deadline), - ENTRY_SECRET: State(pts=session_state.qts, deadline=deadline), - **{s.channel_id: s.pts for s in channel_states} - }, - date=session_state.date, - seq=session_state.seq, - next_deadline=ENTRY_ACCOUNT, - ) + self.map = { + ENTRY_ACCOUNT: State(pts=session_state.pts, deadline=deadline), + ENTRY_SECRET: State(pts=session_state.qts, deadline=deadline), + **{s.channel_id: State(pts=s.pts, deadline=deadline) for s in channel_states} + } + self.date = session_state.date + self.seq = session_state.seq + self.next_deadline = ENTRY_ACCOUNT - @classmethod def session_state(self): """ - Return the current state in a format that sessions understand. + Return the current state. This should be used for persisting the state. """ - return SessionState( - user_id=0, - dc_id=0, - bot=False, - pts=self.map.get(ENTRY_ACCOUNT, 0), - qts=self.map.get(ENTRY_SECRET, 0), + return dict( + pts=self.map[ENTRY_ACCOUNT].pts if ENTRY_ACCOUNT in self.map else 0, + qts=self.map[ENTRY_SECRET].pts if ENTRY_SECRET in self.map else 0, date=self.date, seq=self.seq, - takeout_id=None, - ), [ChannelState(channel_id=id, pts=pts) for id, pts in self.map.items() if isinstance(id, int)] + ), {id: state.pts for id, state in self.map.items() if isinstance(id, int)} def is_empty(self) -> bool: """ Return true if the message box is empty and has no state yet. """ - return self.map.get(ENTRY_ACCOUNT, NO_SEQ) == NO_SEQ + return ENTRY_ACCOUNT not in self.map or self.map[ENTRY_ACCOUNT] == NO_SEQ def check_deadlines(self): """ @@ -200,7 +192,7 @@ class MessageBox: if self.possible_gaps: deadline = min(deadline, *(gap.deadline for gap in self.possible_gaps.values())) elif self.next_deadline in self.map: - deadline = min(deadline, self.map[self.next_deadline]) + deadline = min(deadline, self.map[self.next_deadline].deadline) if now > deadline: # Check all expired entries and add them to the list that needs getting difference. From 4b85ced1e10193812743fff522c15e12d0733a3e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 23 Jan 2022 19:53:48 +0100 Subject: [PATCH 159/256] Reimplement catch_up --- telethon/_client/telegramclient.py | 6 ++---- telethon/_client/updates.py | 5 ++++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 4504d589..3a39fb28 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -2901,11 +2901,9 @@ class TelegramClient: @forward_call(updates.catch_up) async def catch_up(self: 'TelegramClient'): """ - "Catches up" on the missed updates while the client was offline. - You should call this method after registering the event handlers - so that the updates it loads can by processed by your script. + Forces the client to "catch-up" on missed updates. - This can also be used to forcibly fetch new updates if there are any. + The method does not wait for all updates to be received. Example .. code-block:: python diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index e3e8fd78..cf26e809 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -80,7 +80,10 @@ def list_event_handlers(self: 'TelegramClient')\ return [(callback, event) for event, callback in self._event_builders] async def catch_up(self: 'TelegramClient'): - pass + # The update loop is probably blocked on either timeout or an update to arrive. + # Unblock the loop by pushing a dummy update which will always trigger a gap. + # This, in return, causes the update loop to catch up. + await self._updates_queue.put(_tl.UpdatesTooLong()) async def _update_loop(self: 'TelegramClient'): try: From 3aa53dd9811038cc9365840beb0f6bdc871bc15c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 24 Jan 2022 10:59:32 +0100 Subject: [PATCH 160/256] Add missing catch_up param to client init --- telethon/_client/telegramclient.py | 1 + 1 file changed, 1 insertion(+) diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 3a39fb28..d78b0ce6 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -2664,6 +2664,7 @@ class TelegramClient: request_retries: int = 4, flood_sleep_threshold: int = 60, # Update handling. + catch_up: bool = False, receive_updates: bool = True, max_queued_updates: int = 100, ): From 4b61ce18ff70d53562911b34f252b5dc08d50ce4 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 24 Jan 2022 11:00:20 +0100 Subject: [PATCH 161/256] Don't store empty pts in messagebox This lets us rely on "not present" for "not initialized", as opposed to having to check not present OR not empty, and helps prevent more bugs. --- telethon/_updates/messagebox.py | 34 ++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/telethon/_updates/messagebox.py b/telethon/_updates/messagebox.py index 232ef446..54c23a49 100644 --- a/telethon/_updates/messagebox.py +++ b/telethon/_updates/messagebox.py @@ -118,7 +118,7 @@ class MessageBox: # Additional fields beyond PTS needed by `ENTRY_ACCOUNT`. date: int = 1 - seq: int = 0 + seq: int = NO_SEQ # Holds the entry with the closest deadline (optimization to avoid recalculating the minimum deadline). next_deadline: object = None # entry @@ -146,11 +146,14 @@ class MessageBox: Create a [`MessageBox`] from a previously known update state. """ deadline = next_updates_deadline() - self.map = { - ENTRY_ACCOUNT: State(pts=session_state.pts, deadline=deadline), - ENTRY_SECRET: State(pts=session_state.qts, deadline=deadline), - **{s.channel_id: State(pts=s.pts, deadline=deadline) for s in channel_states} - } + + self.map.clear() + if session_state.pts != NO_SEQ: + self.map[ENTRY_ACCOUNT] = State(pts=session_state.pts, deadline=deadline) + if session_state.qts != NO_SEQ: + self.map[ENTRY_SECRET] = State(pts=session_state.qts, deadline=deadline) + self.map.update((s.channel_id, State(pts=s.pts, deadline=deadline)) for s in channel_states) + self.date = session_state.date self.seq = session_state.seq self.next_deadline = ENTRY_ACCOUNT @@ -162,8 +165,8 @@ class MessageBox: This should be used for persisting the state. """ return dict( - pts=self.map[ENTRY_ACCOUNT].pts if ENTRY_ACCOUNT in self.map else 0, - qts=self.map[ENTRY_SECRET].pts if ENTRY_SECRET in self.map else 0, + pts=self.map[ENTRY_ACCOUNT].pts if ENTRY_ACCOUNT in self.map else NO_SEQ, + qts=self.map[ENTRY_SECRET].pts if ENTRY_SECRET in self.map else NO_SEQ, date=self.date, seq=self.seq, ), {id: state.pts for id, state in self.map.items() if isinstance(id, int)} @@ -172,7 +175,7 @@ class MessageBox: """ Return true if the message box is empty and has no state yet. """ - return ENTRY_ACCOUNT not in self.map or self.map[ENTRY_ACCOUNT] == NO_SEQ + return ENTRY_ACCOUNT not in self.map def check_deadlines(self): """ @@ -245,8 +248,17 @@ class MessageBox: # updates will be fetched. def set_state(self, state): deadline = next_updates_deadline() - self.map[ENTRY_ACCOUNT] = State(pts=state.pts, deadline=deadline) - self.map[ENTRY_SECRET] = State(pts=state.qts, deadline=deadline) + + if state.pts != NO_SEQ: + self.map[ENTRY_ACCOUNT] = State(pts=state.pts, deadline=deadline) + else: + self.map.pop(ENTRY_ACCOUNT, None) + + if state.qts != NO_SEQ: + self.map[ENTRY_SECRET] = State(pts=state.qts, deadline=deadline) + else: + self.map.pop(ENTRY_SECRET, None) + self.date = state.date self.seq = state.seq From f7ccf8d8438306e3d7691f856c565b7970e6391e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 24 Jan 2022 11:05:27 +0100 Subject: [PATCH 162/256] Fix reset_deadline check in messagebox --- telethon/_updates/messagebox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/_updates/messagebox.py b/telethon/_updates/messagebox.py index 54c23a49..1562cd21 100644 --- a/telethon/_updates/messagebox.py +++ b/telethon/_updates/messagebox.py @@ -220,7 +220,7 @@ class MessageBox: if self.next_deadline == entry: # If the updated deadline was the closest one, recalculate the new minimum. self.next_deadline = min(self.map.items(), key=lambda entry_state: entry_state[1].deadline)[0] - elif deadline < self.map.get(self.next_deadline, 0): + elif self.next_deadline in self.map and deadline < self.map[self.next_deadline].deadline: # If the updated deadline is smaller than the next deadline, change the next deadline to be the new one. self.next_deadline = entry # else an unrelated deadline was updated, so the closest one remains unchanged. From f7754841723211a3ed013b772cf49172ba7b2b9c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 24 Jan 2022 11:05:58 +0100 Subject: [PATCH 163/256] Properly load and save channel state --- telethon/_client/telegrambaseclient.py | 11 ++++++++--- telethon/_updates/entitycache.py | 3 +++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 5c32d086..480263e4 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -14,7 +14,7 @@ from .._crypto import rsa from .._misc import markdown, enums, helpers from .._network import MTProtoSender, Connection, transports from .._sessions import Session, SQLiteSession, MemorySession -from .._sessions.types import DataCenter, SessionState +from .._sessions.types import DataCenter, SessionState, EntityType, ChannelState from .._updates import EntityCache, MessageBox DEFAULT_DC_ID = 2 @@ -235,7 +235,12 @@ async def connect(self: 'TelegramClient') -> None: else: try_fetch_user = self._session_state.user_id == 0 if self._catch_up: - self._message_box.load(self._session_state, await self._session.get_all_channel_states()) + channel_states = await self._session.get_all_channel_states() + self._message_box.load(self._session_state, channel_states) + for state in channel_states: + entity = await self._session.get_entity(EntityType.CHANNEL, state.channel_id) + if entity: + self._entity_cache.put(entity) dc = all_dcs.get(self._session_state.dc_id) if dc is None: @@ -366,7 +371,7 @@ async def _disconnect(self: 'TelegramClient'): session_state, channel_states = self._message_box.session_state() for channel_id, pts in channel_states.items(): - await self._session.insert_channel_state(channel_id, pts) + await self._session.insert_channel_state(ChannelState(channel_id=channel_id, pts=pts)) await self._replace_session_state(**session_state) diff --git a/telethon/_updates/entitycache.py b/telethon/_updates/entitycache.py index ce89eb4f..8dc95693 100644 --- a/telethon/_updates/entitycache.py +++ b/telethon/_updates/entitycache.py @@ -98,3 +98,6 @@ class EntityCache: def get_all_entities(self): return [Entity(ty, id, hash) for id, (hash, ty) in self.hash_map.items()] + + def put(self, entity): + self.hash_map[entity.id] = (entity.access_hash, entity.ty) From b0b1f304361e861878efbb87b3e141eb5bbc23cd Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 24 Jan 2022 11:21:01 +0100 Subject: [PATCH 164/256] Reintroduce keepalive pings in the sender --- telethon/_network/mtprotosender.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/telethon/_network/mtprotosender.py b/telethon/_network/mtprotosender.py index 2c9c1c59..fb6650ce 100644 --- a/telethon/_network/mtprotosender.py +++ b/telethon/_network/mtprotosender.py @@ -2,6 +2,7 @@ import asyncio import collections import struct import logging +import random from . import authenticator from .._misc.messagepacker import MessagePacker @@ -22,6 +23,7 @@ from .. import _tl UPDATE_BUFFER_FULL_WARN_DELAY = 15 * 60 +PING_DELAY = 60 class MTProtoSender: @@ -51,6 +53,7 @@ class MTProtoSender: self._updates_queue = updates_queue self._connect_lock = asyncio.Lock() self._ping = None + self._next_ping = None # Whether the user has explicitly connected or disconnected. # @@ -123,6 +126,7 @@ class MTProtoSender: self._connection = connection await self._connect() self._user_connected = True + self._next_ping = asyncio.get_running_loop().time() + PING_DELAY return True def is_connected(self): @@ -403,15 +407,15 @@ class MTProtoSender: self._reconnecting = True asyncio.create_task(self._reconnect(error)) - def _keepalive_ping(self, rnd_id): + def _trigger_keepalive_ping(self): """ Send a keep-alive ping. If a pong for the last ping was not received yet, this means we're probably not connected. """ - # TODO this is ugly, update loop shouldn't worry about this, sender should if self._ping is None: - self._ping = rnd_id - self.send(_tl.fn.Ping(rnd_id)) + self._ping = random.randrange(-2**63, 2**63) + self.send(_tl.fn.Ping(self._ping)) + self._next_ping = asyncio.get_running_loop().time() + PING_DELAY else: self._start_reconnect(None) @@ -435,7 +439,11 @@ class MTProtoSender: # TODO Wait for the connection send queue to be empty? # This means that while it's not empty we can wait for # more messages to be added to the send queue. - batch, data = await self._send_queue.get() + try: + batch, data = await asyncio.wait_for(self._send_queue.get(), self._next_ping - asyncio.get_running_loop().time()) + except asyncio.TimeoutError: + self._trigger_keepalive_ping() + continue if not data: continue From a25f0199649efcba83f054dfb305b2af6a34733a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 24 Jan 2022 11:29:02 +0100 Subject: [PATCH 165/256] Review, unify and simplify retry_range usage --- telethon/_misc/helpers.py | 18 +++--------------- telethon/_network/mtprotosender.py | 9 ++++----- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/telethon/_misc/helpers.py b/telethon/_misc/helpers.py index a3480007..c6c76b8e 100644 --- a/telethon/_misc/helpers.py +++ b/telethon/_misc/helpers.py @@ -102,23 +102,11 @@ def strip_text(text, entities): return text -def retry_range(retries, force_retry=True): +def retry_range(retries): """ - Generates an integer sequence starting from 1. If `retries` is - not a zero or a positive integer value, the sequence will be - infinite, otherwise it will end at `retries + 1`. + Generates an integer sequence starting from 1, always returning once, and adding the given retries. """ - - # We need at least one iteration even if the retries are 0 - # when force_retry is True. - if force_retry and not (retries is None or retries < 0): - retries += 1 - - attempt = 0 - while attempt != retries: - attempt += 1 - yield attempt - + return range(1, max(retries, 0) + 2) async def _maybe_await(value): diff --git a/telethon/_network/mtprotosender.py b/telethon/_network/mtprotosender.py index fb6650ce..84f2b2c0 100644 --- a/telethon/_network/mtprotosender.py +++ b/telethon/_network/mtprotosender.py @@ -249,9 +249,9 @@ class MTProtoSender: break # all steps done, break retry loop else: if not connected: - raise ConnectionError('Connection to Telegram failed {} time(s)'.format(self._retries)) + raise ConnectionError('Connection to Telegram failed {} time(s)'.format(1 + self._retries)) - e = ConnectionError('auth_key generation failed {} time(s)'.format(self._retries)) + e = ConnectionError('auth_key generation failed {} time(s)'.format(1 + self._retries)) await self._disconnect(error=e) raise e @@ -349,12 +349,11 @@ class MTProtoSender: # Start with a clean state (and thus session ID) to avoid old msgs self._state.reset() - retries = self._retries if self._auto_reconnect else 0 + retry_range = helpers.retry_range(self._retries) if self._auto_reconnect else range(0) attempt = 0 ok = True - # We're already "retrying" to connect, so we don't want to force retries - for attempt in helpers.retry_range(retries, force_retry=False): + for attempt in retry_range: try: await self._connect() except (IOError, asyncio.TimeoutError) as e: From 539e3cb8081acbd9a5cc7a61c0731ca62842597e Mon Sep 17 00:00:00 2001 From: Devesh Pal Date: Mon, 24 Jan 2022 17:45:02 +0530 Subject: [PATCH 166/256] Add new features from new layer (#3676) Updated some documentation regarding raw API. get_permissions has been adjusted. Expose more parameters when sending messages. Update chat action. Support sending spoilers. Update buttons. --- readthedocs/examples/users.rst | 2 +- telethon/_client/chats.py | 9 ++------ telethon/_client/messages.py | 16 +++++++++---- telethon/_client/uploads.py | 9 ++++++-- telethon/_events/chataction.py | 14 ++++++++++-- telethon/_misc/html.py | 2 ++ telethon/_misc/markdown.py | 1 + telethon/types/_custom/button.py | 30 +++++++++++++++++++++++-- telethon/types/_custom/inlineresult.py | 8 +++++-- telethon/types/_custom/message.py | 12 ++++++++-- telethon/types/_custom/messagebutton.py | 8 ++++++- 11 files changed, 88 insertions(+), 23 deletions(-) diff --git a/readthedocs/examples/users.rst b/readthedocs/examples/users.rst index d9c648ae..ea83871d 100644 --- a/readthedocs/examples/users.rst +++ b/readthedocs/examples/users.rst @@ -25,7 +25,7 @@ you should use :tl:`GetFullUser`: # or even full = await client(GetFullUserRequest('username')) - bio = full.about + bio = full.full_user.about See :tl:`UserFull` to know what other fields you can access. diff --git a/telethon/_client/chats.py b/telethon/_client/chats.py index 0759acc2..cd6d9726 100644 --- a/telethon/_client/chats.py +++ b/telethon/_client/chats.py @@ -634,13 +634,8 @@ async def get_permissions( entity = await self.get_entity(entity) if not user: - if isinstance(entity, _tl.Channel): - FullChat = await self(_tl.fn.channels.GetFullChannel(entity)) - elif isinstance(entity, _tl.Chat): - FullChat = await self(_tl.fn.messages.GetFullChat(entity)) - else: - return - return FullChat.chats[0].default_banned_rights + if helpers._entity_type(entity) != helpers._EntityType.USER: + return entity.default_banned_rights entity = await self.get_input_entity(entity) user = await self.get_input_entity(user) diff --git a/telethon/_client/messages.py b/telethon/_client/messages.py index e85921f3..250582d0 100644 --- a/telethon/_client/messages.py +++ b/telethon/_client/messages.py @@ -426,8 +426,10 @@ async def send_message( ttl: int = None, # - Send options reply_to: 'typing.Union[int, _tl.Message]' = None, + send_as: 'hints.EntityLike' = None, clear_draft: bool = False, background: bool = None, + noforwards: bool = None, schedule: 'hints.DateLike' = None, comment_to: 'typing.Union[int, _tl.Message]' = None, ) -> '_tl.Message': @@ -483,7 +485,7 @@ async def send_message( entity, message._file._media, reply_to_msg_id=reply_to, message=message._text, entities=message._fmt_entities, reply_markup=message._reply_markup, silent=message._silent, schedule_date=schedule, clear_draft=clear_draft, - background=background + background=background, noforwards=noforwards, send_as=send_as ) else: request = _tl.fn.messages.SendMessage( @@ -496,7 +498,9 @@ async def send_message( silent=silent, background=background, reply_markup=_custom.button.build_reply_markup(buttons), - schedule_date=schedule + schedule_date=schedule, + noforwards=noforwards, + send_as=send_as ) result = await self(request) @@ -525,7 +529,9 @@ async def forward_messages( with_my_score: bool = None, silent: bool = None, as_album: bool = None, - schedule: 'hints.DateLike' = None + schedule: 'hints.DateLike' = None, + noforwards: bool = None, + send_as: 'hints.EntityLike' = None ) -> 'typing.Sequence[_tl.Message]': if as_album is not None: warnings.warn('the as_album argument is deprecated and no longer has any effect') @@ -565,7 +571,9 @@ async def forward_messages( silent=silent, background=background, with_my_score=with_my_score, - schedule_date=schedule + schedule_date=schedule, + noforwards=noforwards, + send_as=send_as ) result = await self(req) sent.extend(self._get_response_message(req, result, entity)) diff --git a/telethon/_client/uploads.py b/telethon/_client/uploads.py index 92a2d36e..a92df8f4 100644 --- a/telethon/_client/uploads.py +++ b/telethon/_client/uploads.py @@ -113,6 +113,8 @@ async def send_file( reply_to: 'typing.Union[int, _tl.Message]' = None, clear_draft: bool = False, background: bool = None, + noforwards: bool = None, + send_as: 'hints.EntityLike' = None, schedule: 'hints.DateLike' = None, comment_to: 'typing.Union[int, _tl.Message]' = None, ) -> '_tl.Message': @@ -146,13 +148,16 @@ async def send_file( background=background, schedule=schedule, comment_to=comment_to, + noforwards=noforwards, + send_as=send_as ) async def _send_album(self: 'TelegramClient', entity, files, caption='', progress_callback=None, reply_to=None, parse_mode=(), silent=None, schedule=None, supports_streaming=None, clear_draft=None, - force_document=False, background=None, ttl=None): + force_document=False, background=None, ttl=None, + send_as=None, noforwards=None): """Specialized version of .send_file for albums""" # We don't care if the user wants to avoid cache, we will use it # anyway. Why? The cached version will be exactly the same thing @@ -212,7 +217,7 @@ async def _send_album(self: 'TelegramClient', entity, files, caption='', request = _tl.fn.messages.SendMultiMedia( entity, reply_to_msg_id=reply_to, multi_media=media, silent=silent, schedule_date=schedule, clear_draft=clear_draft, - background=background + background=background, noforwards=noforwards, send_as=send_as ) result = await self(request) diff --git a/telethon/_events/chataction.py b/telethon/_events/chataction.py index 671347c0..0bf83aa1 100644 --- a/telethon/_events/chataction.py +++ b/telethon/_events/chataction.py @@ -76,6 +76,11 @@ class ChatAction(EventBuilder): return cls.Event(msg, added_by=added_by, users=action.users) + elif isinstance(action, _tl.MessageActionChatJoinedByRequest): + # user joined from join request (after getting admin approval) + return cls.Event(msg, + from_approval=True, + users=msg.from_id) elif isinstance(action, _tl.MessageActionChatDeleteUser): return cls.Event(msg, kicked_by=utils.get_peer_id(msg.from_id) if msg.from_id else True, @@ -138,6 +143,10 @@ class ChatAction(EventBuilder): user_kicked (`bool`): `True` if the user was kicked by some other. + user_approved (`bool`): + `True` if the user's join request was approved. + along with `user_joined` will be also True. + created (`bool`, optional): `True` if this chat was just created. @@ -152,7 +161,7 @@ class ChatAction(EventBuilder): """ def __init__(self, where, new_photo=None, - added_by=None, kicked_by=None, created=None, + added_by=None, kicked_by=None, created=None, from_approval=None, users=None, new_title=None, pin_ids=None, pin=None, new_score=None): if isinstance(where, _tl.MessageService): self.action_message = where @@ -177,11 +186,12 @@ class ChatAction(EventBuilder): self.user_added = self.user_joined = self.user_left = \ self.user_kicked = self.unpin = False - if added_by is True: + if added_by is True or from_approval is True: self.user_joined = True elif added_by: self.user_added = True self._added_by = added_by + self.user_approved = from_approval # If `from_id` was not present (it's `True`) or the affected # user was "kicked by itself", then it left. Else it was kicked. diff --git a/telethon/_misc/html.py b/telethon/_misc/html.py index c17f6f7c..cdf4ced4 100644 --- a/telethon/_misc/html.py +++ b/telethon/_misc/html.py @@ -47,6 +47,8 @@ class HTMLToTelegramParser(HTMLParser): EntityType = _tl.MessageEntityUnderline elif tag == 'del' or tag == 's': EntityType = _tl.MessageEntityStrike + elif tag == 'tg-spoiler': + EntityType = _tl.MessageEntitySpoiler elif tag == 'blockquote': EntityType = _tl.MessageEntityBlockquote elif tag == 'code': diff --git a/telethon/_misc/markdown.py b/telethon/_misc/markdown.py index 8dc82701..3b62c995 100644 --- a/telethon/_misc/markdown.py +++ b/telethon/_misc/markdown.py @@ -19,6 +19,7 @@ DELIMITERS = { _tl.MessageEntityCode: ('`', '`'), _tl.MessageEntityItalic: ('_', '_'), _tl.MessageEntityStrike: ('~~', '~~'), + _tl.MessageEntitySpoiler: ('||', '||'), _tl.MessageEntityUnderline: ('# ', ''), } diff --git a/telethon/types/_custom/button.py b/telethon/types/_custom/button.py index ffac7c99..27f1aae9 100644 --- a/telethon/types/_custom/button.py +++ b/telethon/types/_custom/button.py @@ -1,6 +1,8 @@ +import typing + from .messagebutton import MessageButton from ... import _tl -from ..._misc import utils +from ..._misc import utils, hints class Button: @@ -54,6 +56,7 @@ class Button: _tl.KeyboardButtonCallback, _tl.KeyboardButtonGame, _tl.KeyboardButtonSwitchInline, + _tl.KeyboardButtonUserProfile, _tl.KeyboardButtonUrl, _tl.InputKeyboardButtonUrlAuth )) @@ -166,6 +169,29 @@ class Button: fwd_text=fwd_text ) + @staticmethod + def mention(text, input_entity): + """ + Creates a new inline button linked to the profile of user. + + Args: + input_entity: + Input entity of :tl:User to use for profile button. + By default, this is the logged in user (itself), although + you may pass a different input peer. + + .. note:: + + For now, you cannot use ID or username for this argument. + If you want to use different user, you must manually use + `client.get_input_entity() `. + """ + return _tl.InputKeyboardButtonUserProfile( + text, + utils.get_input_user(input_entity or _tl.InputUserSelf()) + ) + + @classmethod def text(cls, text, *, resize=None, single_use=None, selective=None): """ @@ -387,4 +413,4 @@ def build_reply_markup( return _tl.ReplyInlineMarkup(rows) # elif is_normal: return _tl.ReplyKeyboardMarkup( - rows, resize=resize, single_use=single_use, selective=selective) + rows, resize=resize, single_use=single_use, selective=selective) \ No newline at end of file diff --git a/telethon/types/_custom/inlineresult.py b/telethon/types/_custom/inlineresult.py index 052be4dd..45867edb 100644 --- a/telethon/types/_custom/inlineresult.py +++ b/telethon/types/_custom/inlineresult.py @@ -104,7 +104,7 @@ class InlineResult: async def click(self, entity=None, reply_to=None, comment_to=None, silent=False, clear_draft=False, hide_via=False, - background=None): + background=None, send_as=None): """ Clicks this result and sends the associated `message`. @@ -137,6 +137,9 @@ class InlineResult: background (`bool`, optional): Whether the message should be send in background. + send_as (`entity`, optional): + The channel entity on behalf of which, message should be send. + """ if entity: entity = await self._client.get_input_entity(entity) @@ -158,7 +161,8 @@ class InlineResult: background=background, clear_draft=clear_draft, hide_via=hide_via, - reply_to_msg_id=reply_id + reply_to_msg_id=reply_id, + send_as=send_as ) return self._client._get_response_message( req, await self._client(req), entity) diff --git a/telethon/types/_custom/message.py b/telethon/types/_custom/message.py index c1e213aa..e1bfff41 100644 --- a/telethon/types/_custom/message.py +++ b/telethon/types/_custom/message.py @@ -189,6 +189,10 @@ class Message(ChatGetter, SenderGetter): The number of times this message has been forwarded. """) + noforwards = _fwd('noforwards', """ + does the message was sent with noforwards restriction. + """) + replies = _fwd('replies', """ The number of times another message has replied to this message. """) @@ -205,13 +209,17 @@ class Message(ChatGetter, SenderGetter): grouped_id = _fwd('grouped_id', """ If this message belongs to a group of messages (photo albums or video albums), all of them will - have the same value here. + have the same value here.""") - restriction_reason (List[:tl:`RestrictionReason`]) + restriction_reason = _fwd('restriction_reason', """ An optional list of reasons why this message was restricted. If the list is `None`, this message has not been restricted. """) + reactions = _fwd('reactions', """ + emoji reactions attached to the message. + """) + ttl_period = _fwd('ttl_period', """ The Time To Live period configured for this message. The message should be erased from wherever it's stored (memory, a diff --git a/telethon/types/_custom/messagebutton.py b/telethon/types/_custom/messagebutton.py index 2d588727..eee4486e 100644 --- a/telethon/types/_custom/messagebutton.py +++ b/telethon/types/_custom/messagebutton.py @@ -71,6 +71,10 @@ class MessageButton: If it's an inline :tl:`KeyboardButtonCallback` with text and data, it will be "clicked" and the :tl:`BotCallbackAnswer` returned. + If it's an inline :tl:`KeyboardButtonUserProfile` button, the + `client.get_entity` will be called and the resulting :tl:User will be + returned. + If it's an inline :tl:`KeyboardButtonSwitchInline` button, the :tl:`StartBot` will be invoked and the resulting updates returned. @@ -107,6 +111,8 @@ class MessageButton: return await self._client(req) except BotResponseTimeoutError: return None + elif isinstance(self.button, _tl.KeyboardButtonUserProfile): + return await self._client.get_entity(self.button.user_id) elif isinstance(self.button, _tl.KeyboardButtonSwitchInline): return await self._client(_tl.fn.messages.StartBot( bot=self._bot, peer=self._chat, start_param=self.button.query @@ -143,4 +149,4 @@ class MessageButton: long, lat = share_geo share_geo = _tl.InputMediaGeoPoint(_tl.InputGeoPoint(lat=lat, long=long)) - return await self._client.send_file(self._chat, share_geo) + return await self._client.send_file(self._chat, share_geo) \ No newline at end of file From 805898c2fdda72ba52431a49822166cbbe71f65c Mon Sep 17 00:00:00 2001 From: vladislav doster Date: Mon, 24 Jan 2022 06:15:32 -0600 Subject: [PATCH 167/256] Improve readability for readthedocs/concepts/entities.rst (#3259) --- readthedocs/concepts/entities.rst | 45 +++++++++++++++---------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/readthedocs/concepts/entities.rst b/readthedocs/concepts/entities.rst index 40bfac30..f76c3983 100644 --- a/readthedocs/concepts/entities.rst +++ b/readthedocs/concepts/entities.rst @@ -39,7 +39,7 @@ A lot of methods and requests require *entities* to work. For example, you send a message to an *entity*, get the username of an *entity*, and so on. -There are a lot of things that work as entities: usernames, phone numbers, +There are many things that work as entities: usernames, phone numbers, chat links, invite links, IDs, and the types themselves. That is, you can use any of those when you see an "entity" is needed. @@ -58,16 +58,16 @@ You should use, **from better to worse**: ``entity = await client.get_input_entity(...)``. 2. Entities. For example, if you had to get someone's - username, you can just use ``user`` or ``channel``. + username, you can use ``user`` or ``channel``. It will work. Only use this option if you already have the entity! 3. IDs. This will always look the entity up from the cache (the ``*.session`` file caches seen entities). -4. Usernames, phone numbers and links. The cache will be +4. Usernames, phone numbers, and links. The cache will be used too (unless you force a `client.get_entity() `), - but may make a request if the username, phone or link + but may make a request if the username, phone, or link has not been found yet. In recent versions of the library, the following two are equivalent: @@ -148,7 +148,7 @@ become possible. Every entity the library encounters (in any response to any call) will by default be cached in the ``.session`` file (an SQLite database), to avoid -performing unnecessary API calls. If the entity cannot be found, additonal +performing unnecessary API calls. If the entity cannot be found, additional calls like :tl:`ResolveUsernameRequest` or :tl:`GetContactsRequest` may be made to obtain the required information. @@ -158,24 +158,23 @@ Entities vs. Input Entities .. note:: - This section is informative, but worth reading. The library + This section is informative but worth reading. The library will transparently handle all of these details for you. On top of the normal types, the API also make use of what they call their ``Input*`` versions of objects. The input version of an entity (e.g. :tl:`InputPeerUser`, :tl:`InputChat`, etc.) only contains the minimum -information that's required from Telegram to be able to identify +information required from Telegram to identify who you're referring to: a :tl:`Peer`'s **ID** and **hash**. They are named like this because they are input parameters in the requests. -Entities' ID are the same for all user and bot accounts, however, the access +Entities' IDs are the same for all user and bot accounts. However, the access hash is **different for each account**, so trying to reuse the access hash from one account in another will **not** work. -Sometimes, Telegram only needs to indicate the type of the entity along -with their ID. For this purpose, :tl:`Peer` versions of the entities also +Sometimes, Telegram only needs to indicate the entity type and their ID. For this purpose, :tl:`Peer` versions of the entities also exist, which just have the ID. You cannot get the hash out of them since -you should not be needing it. The library probably has cached it before. +you should not need it. The library probably has cached it before. Peers are enough to identify an entity, but they are not enough to make a request with them. You need to know their hash before you can @@ -186,15 +185,15 @@ be in your dialogs, participants, message forwards, etc. You *can* use peers with the library. Behind the scenes, they are replaced with the input variant. Peers "aren't enough" on their own - but the library will do some more work to use the right type. + , but the library will do some more work to use the right type. As we just mentioned, API calls don't need to know the whole information about the entities, only their ID and hash. For this reason, another method, `client.get_input_entity() ` is available. This will always use the cache while possible, making zero API calls most of the time. When a request is made, if you provided the full -entity, e.g. an :tl:`User`, the library will convert it to the required -:tl:`InputPeer` automatically for you. +entity, e.g. an :tl:`User`, the library will automatically convert it to the required +:tl:`InputPeer`. **You should always favour** `client.get_input_entity() ` @@ -216,9 +215,9 @@ wherever needed, so you can even do things like: await client(SendMessageRequest('username', 'hello')) -The library will call the ``.resolve()`` method of the request, which will -resolve ``'username'`` with the appropriated :tl:`InputPeer`. Don't worry if -you don't get this yet, but remember some of the details here are important. +The library will call the request's ``.resolve()`` method, which will +resolve ``'username'`` with the appropriate :tl:`InputPeer`. Don't worry if +you don't get this yet, but remember that some of the details here are important. Full Entities @@ -231,8 +230,8 @@ This full variant has additional information such as whether the user is blocked, its notification settings, the bio or about of the user, etc. There is also :tl:`messages.ChatFull` which is the equivalent of full entities -for chats and channels, with also the about section of the channel. Note that -the ``users`` field only contains bots for the channel (so that clients can +for chats and channels, with the about section of the channel. Note that +the ``users`` field only contains bots for the channel (so clients can suggest commands to use). You can get both of these by invoking :tl:`GetFullUser`, :tl:`GetFullChat` @@ -282,7 +281,7 @@ Summary ======= TL;DR; If you're here because of *"Could not find the input entity for"*, -you must ask yourself "how did I find this entity through official +you must ask yourself, "how did I find this entity through official applications"? Now do the same with the library. Use what applies: .. code-block:: python @@ -295,13 +294,13 @@ applications"? Now do the same with the library. Use what applies: # Do you have a conversation open with them? Get dialogs. await client.get_dialogs() - # Are they participant of some group? Get them. + # Are they participants of some group? Get them. await client.get_participants('username') # Is the entity the original sender of a forwarded message? Get it. await client.get_messages('username', 100) - # NOW you can use the ID, anywhere! + # NOW you can use the ID anywhere! await client.send_message(123456, 'Hi!') entity = await client.get_entity(123456) @@ -310,4 +309,4 @@ applications"? Now do the same with the library. Use what applies: Once the library has "seen" the entity, you can use their **integer** ID. You can't use entities from IDs the library hasn't seen. You must make the library see them *at least once* and disconnect properly. You know where -the entities are and you must tell the library. It won't guess for you. +the entities are, and you must tell the library. It won't guess for you. From 530284a68bed8e6b0c7a789b47851af5eb6c4b4e Mon Sep 17 00:00:00 2001 From: Shrimadhav U K Date: Mon, 24 Jan 2022 17:45:49 +0530 Subject: [PATCH 168/256] Slight rewording of error message (#3256) --- telethon_generator/data/errors.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon_generator/data/errors.csv b/telethon_generator/data/errors.csv index 89546bc7..caf9b5e6 100644 --- a/telethon_generator/data/errors.csv +++ b/telethon_generator/data/errors.csv @@ -297,7 +297,7 @@ RANDOM_ID_INVALID,400,A provided random ID is invalid RANDOM_LENGTH_INVALID,400,Random length invalid RANGES_INVALID,400,Invalid range provided REACTION_EMPTY,400,No reaction provided -REACTION_INVALID,400,Invalid reaction provided (only emoji are allowed) +REACTION_INVALID,400,Invalid reaction provided (only emoji are allowed) or you cannot use the reaction in the specified chat REFLECTOR_NOT_AVAILABLE,400,Invalid call reflector server REG_ID_GENERATE_FAILED,500,Failure while generating registration ID REPLY_MARKUP_GAME_EMPTY,400,The provided reply markup for the game is empty From 674b7d68a53e55e391cc09005994ca0a839b8f25 Mon Sep 17 00:00:00 2001 From: Danny Lin Date: Mon, 24 Jan 2022 04:17:22 -0800 Subject: [PATCH 169/256] Add support for new invite link format (#3213) --- telethon/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/utils.py b/telethon/utils.py index 7d058518..ac5db1c1 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -54,7 +54,7 @@ mimetypes.add_type('audio/flac', '.flac') mimetypes.add_type('application/x-tgsticker', '.tgs') USERNAME_RE = re.compile( - r'@|(?:https?://)?(?:www\.)?(?:telegram\.(?:me|dog)|t\.me)/(@|joinchat/)?' + r'@|(?:https?://)?(?:www\.)?(?:telegram\.(?:me|dog)|t\.me)/(@|\+|joinchat/)?' ) TG_JOIN_RE = re.compile( r'tg://(join)\?invite=' From d9e2a8a5ca457c3afe295ea0ffb87c930f398144 Mon Sep 17 00:00:00 2001 From: penn5 Date: Mon, 24 Jan 2022 13:25:58 +0000 Subject: [PATCH 170/256] Avoid usage of __builtins__ in the generator (#3161) --- telethon_generator/generators/tlobject.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telethon_generator/generators/tlobject.py b/telethon_generator/generators/tlobject.py index e5a3d07e..c1e5e14a 100644 --- a/telethon_generator/generators/tlobject.py +++ b/telethon_generator/generators/tlobject.py @@ -1,3 +1,4 @@ +import builtins import functools import os import re @@ -213,7 +214,7 @@ def _write_class_init(tlobject, kind, type_constructors, builder): if not tlobject.real_args: return - if any(a.name in __builtins__ for a in tlobject.real_args): + if any(a.name in dir(builtins) for a in tlobject.real_args): builder.writeln('# noinspection PyShadowingBuiltins') builder.writeln("def __init__({}):", ', '.join(['self'] + args)) From 4aa23cfbbc6861e12ec6956e868055b5e6bbcb2b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 24 Jan 2022 14:25:16 +0100 Subject: [PATCH 171/256] Fix flood_sleep_threshold was not being forwarded Closes #3160. --- telethon/_client/users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/_client/users.py b/telethon/_client/users.py index 9f381af6..53da7574 100644 --- a/telethon/_client/users.py +++ b/telethon/_client/users.py @@ -28,7 +28,7 @@ def _fmt_flood(delay, request, *, early=False, td=datetime.timedelta): async def call(self: 'TelegramClient', request, ordered=False, flood_sleep_threshold=None): - return await _call(self, self._sender, request, ordered=ordered) + return await _call(self, self._sender, request, ordered=ordered, flood_sleep_threshold=flood_sleep_threshold) async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sleep_threshold=None): From 3c7bb53c3ca310fc26f5bdba428c9541b4a3fdb4 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 24 Jan 2022 14:27:34 +0100 Subject: [PATCH 172/256] Use threads for factorization Closes #3162. --- telethon/_network/authenticator.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/telethon/_network/authenticator.py b/telethon/_network/authenticator.py index dfd16469..2a6890d7 100644 --- a/telethon/_network/authenticator.py +++ b/telethon/_network/authenticator.py @@ -2,6 +2,8 @@ This module contains several functions that authenticate the client machine with Telegram's servers, effectively creating an authorization key. """ +import asyncio +import functools import os import time from hashlib import sha1 @@ -31,7 +33,10 @@ async def do_authentication(sender): pq = get_int(res_pq.pq) # Step 2 sending: DH Exchange - p, q = Factorization.factorize(pq) + p, q = await asyncio.get_running_loop().run_in_executor( + None, + functools.partial(Factorization.factorize, pq) + ) p, q = rsa.get_byte_array(p), rsa.get_byte_array(q) new_nonce = int.from_bytes(os.urandom(32), 'little', signed=True) From 0eadca670f055a94bd0a10c3f8fb438982fade6c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 24 Jan 2022 14:32:45 +0100 Subject: [PATCH 173/256] Document more RPC errors Closes #3163. --- telethon_generator/data/errors.csv | 7 +++++++ telethon_generator/data/methods.csv | 11 ++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/telethon_generator/data/errors.csv b/telethon_generator/data/errors.csv index 04b350f8..cc117c92 100644 --- a/telethon_generator/data/errors.csv +++ b/telethon_generator/data/errors.csv @@ -41,6 +41,7 @@ BOT_MISSING,400,This method can only be run by a bot BOT_PAYMENTS_DISABLED,400,This method can only be run by a bot BOT_POLLS_DISABLED,400,You cannot create polls under a bot account BOT_RESPONSE_TIMEOUT,400,The bot did not answer to the callback query in time +BOT_SCORE_NOT_MODIFIED,400, BROADCAST_CALLS_DISABLED,400, BROADCAST_FORBIDDEN,403,The request cannot be used in broadcast channels BROADCAST_ID_INVALID,400,The channel is invalid @@ -140,6 +141,7 @@ FILE_REFERENCE_EXPIRED,400,The file reference has expired and is no longer valid FILE_REFERENCE_INVALID,400,The file reference is invalid or you can't do that operation on such message FILE_TITLE_EMPTY,400, FIRSTNAME_INVALID,400,The first name is invalid +FILTER_NOT_SUPPORTED,400, FLOOD_TEST_PHONE_WAIT_0,420,A wait of {seconds} seconds is required in the test servers FLOOD_WAIT_0,420,A wait of {seconds} seconds is required FOLDER_ID_EMPTY,400,The folder you tried to delete was already empty @@ -274,6 +276,7 @@ PHOTO_SAVE_FILE_INVALID,400,The photo you tried to send cannot be saved by Teleg PHOTO_THUMB_URL_EMPTY,400,The URL used as a thumbnail appears to be empty or has caused another HTTP error PIN_RESTRICTED,400,You can't pin messages in private chats with other people PINNED_DIALOGS_TOO_MUCH,400, +POLL_ANSWER_INVALID,400, POLL_ANSWERS_INVALID,400,The poll did not have enough answers or had too many POLL_OPTION_DUPLICATE,400,A duplicate option was sent in the same poll POLL_OPTION_INVALID,400,A poll option used invalid data (the data may be too long) @@ -310,6 +313,7 @@ RESULTS_TOO_MUCH,400,"You sent too many results, see https://core.telegram.org/b RESULT_ID_DUPLICATE,400,Duplicated IDs on the sent results. Make sure to use unique IDs RESULT_ID_INVALID,400,The given result cannot be used to send the selection to the bot RESULT_TYPE_INVALID,400,Result type invalid +REVOTE_NOT_ALLOWED,400, RIGHT_FORBIDDEN,403,Either your admin rights do not allow you to do this or you passed the wrong rights combination (some rights only apply to channels and vice versa) RPC_CALL_FAIL,500,"Telegram is having internal issues, please try again later." RPC_MCGET_FAIL,500,"Telegram is having internal issues, please try again later." @@ -319,6 +323,7 @@ SCHEDULE_DATE_INVALID,400, SCHEDULE_DATE_TOO_LATE,400,The date you tried to schedule is too far in the future (last known limit of 1 year and a few hours) SCHEDULE_STATUS_PRIVATE,400,You cannot schedule a message until the person comes online if their privacy does not show this information SCHEDULE_TOO_MUCH,400,You cannot schedule more messages in this chat (last known limit of 100 per chat) +SCORE_INVALID,400, SEARCH_QUERY_EMPTY,400,The search query is empty SECONDS_INVALID,400,"Slow mode only supports certain values (e.g. 0, 10s, 30s, 1m, 5m, 15m and 1h)" SEND_AS_PEER_INVALID,, @@ -334,6 +339,7 @@ SHA256_HASH_INVALID,400,The provided SHA256 hash is invalid SHORTNAME_OCCUPY_FAILED,400,An error occurred when trying to register the short-name used for the sticker pack. Try a different name SHORT_NAME_INVALID,400, SHORT_NAME_OCCUPIED,400, +SLOWMODE_MULTI_MSGS_DISABLED,400, SLOWMODE_WAIT_0,420,A wait of {seconds} seconds is required before sending another message in this chat SRP_ID_INVALID,400, START_PARAM_EMPTY,400,The start parameter is empty @@ -376,6 +382,7 @@ UNKNOWN_METHOD,500,The method you tried to call cannot be called on non-CDN DCs UNTIL_DATE_INVALID,400,That date cannot be specified in this request (try using None) UPDATE_APP_TO_LOGIN,406, URL_INVALID,400,The URL used was invalid (e.g. when answering a callback with a URL that's not t.me/yourbot or your game's URL) +USAGE_LIMIT_INVALID,400, USER_VOLUME_INVALID,400, USERNAME_INVALID,400,"Nobody is using this username, or the username is unacceptable. If the latter, it must match r""[a-zA-Z][\w\d]{3,30}[a-zA-Z\d]""" USERNAME_NOT_MODIFIED,400,The username is not different from the current username diff --git a/telethon_generator/data/methods.csv b/telethon_generator/data/methods.csv index fff32139..3088b65b 100644 --- a/telethon_generator/data/methods.csv +++ b/telethon_generator/data/methods.csv @@ -197,9 +197,9 @@ messages.editChatPhoto,both,CHAT_ID_INVALID INPUT_CONSTRUCTOR_INVALID INPUT_FETC messages.editChatTitle,both,CHAT_ID_INVALID NEED_CHAT_INVALID messages.editInlineBotMessage,both,MESSAGE_ID_INVALID MESSAGE_NOT_MODIFIED messages.editMessage,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_WRITE_FORBIDDEN INLINE_BOT_REQUIRED INPUT_USER_DEACTIVATED MEDIA_GROUPED_INVALID MEDIA_NEW_INVALID MEDIA_PREV_INVALID MESSAGE_AUTHOR_REQUIRED MESSAGE_EDIT_TIME_EXPIRED MESSAGE_EMPTY MESSAGE_ID_INVALID MESSAGE_NOT_MODIFIED PEER_ID_INVALID -messages.exportChatInvite,both,CHAT_ID_INVALID EXPIRE_DATE_INVALID +messages.exportChatInvite,both,CHAT_ID_INVALID EXPIRE_DATE_INVALID USAGE_LIMIT_INVALID messages.faveSticker,user,STICKER_ID_INVALID -messages.forwardMessages,both,BROADCAST_PUBLIC_VOTERS_FORBIDDEN CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_ID_INVALID CHAT_SEND_GIFS_FORBIDDEN CHAT_SEND_MEDIA_FORBIDDEN CHAT_SEND_STICKERS_FORBIDDEN CHAT_WRITE_FORBIDDEN GROUPED_MEDIA_INVALID INPUT_USER_DEACTIVATED MEDIA_EMPTY MESSAGE_IDS_EMPTY MESSAGE_ID_INVALID PEER_ID_INVALID PTS_CHANGE_EMPTY QUIZ_ANSWER_MISSING RANDOM_ID_DUPLICATE RANDOM_ID_INVALID SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH TIMEOUT USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT YOU_BLOCKED_USER +messages.forwardMessages,both,BROADCAST_PUBLIC_VOTERS_FORBIDDEN CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_ID_INVALID CHAT_SEND_GIFS_FORBIDDEN CHAT_SEND_MEDIA_FORBIDDEN CHAT_SEND_STICKERS_FORBIDDEN CHAT_WRITE_FORBIDDEN GROUPED_MEDIA_INVALID INPUT_USER_DEACTIVATED MEDIA_EMPTY MESSAGE_IDS_EMPTY MESSAGE_ID_INVALID PEER_ID_INVALID PTS_CHANGE_EMPTY QUIZ_ANSWER_MISSING RANDOM_ID_DUPLICATE RANDOM_ID_INVALID SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH SLOWMODE_MULTI_MSGS_DISABLED TIMEOUT USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT YOU_BLOCKED_USER messages.getAllChats,user, messages.getAllDrafts,user, messages.getAllStickers,user, @@ -241,6 +241,7 @@ messages.getSavedGifs,user, messages.getScheduledHistory,user, messages.getScheduledMessages,user, messages.getSearchCounters,user, +messages.getSearchResultsCalendar,user,FILTER_NOT_SUPPORTED messages.getSplitRanges,user, messages.getStatsURL,user, messages.getStickerSet,both,EMOTICON_STICKERPACK_MISSING STICKERSET_INVALID @@ -280,17 +281,17 @@ messages.sendEncrypted,user,CHAT_ID_INVALID DATA_INVALID ENCRYPTION_DECLINED MSG messages.sendEncryptedFile,user,MSG_WAIT_FAILED messages.sendEncryptedService,user,DATA_INVALID ENCRYPTION_DECLINED MSG_WAIT_FAILED USER_IS_BLOCKED messages.sendInlineBotResult,user,CHAT_SEND_INLINE_FORBIDDEN CHAT_WRITE_FORBIDDEN INLINE_RESULT_EXPIRED PEER_ID_INVALID QUERY_ID_EMPTY SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH WEBPAGE_CURL_FAILED WEBPAGE_MEDIA_EMPTY -messages.sendMedia,both,BOT_PAYMENTS_DISABLED BOT_POLLS_DISABLED BROADCAST_PUBLIC_VOTERS_FORBIDDEN CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_SEND_MEDIA_FORBIDDEN CHAT_WRITE_FORBIDDEN CURRENCY_TOTAL_AMOUNT_INVALID EMOTICON_INVALID EXTERNAL_URL_INVALID FILE_PARTS_INVALID FILE_PART_LENGTH_INVALID FILE_REFERENCE_EMPTY FILE_REFERENCE_EXPIRED GAME_BOT_INVALID INPUT_USER_DEACTIVATED MEDIA_CAPTION_TOO_LONG MEDIA_EMPTY PAYMENT_PROVIDER_INVALID PEER_ID_INVALID PHOTO_EXT_INVALID PHOTO_INVALID_DIMENSIONS PHOTO_SAVE_FILE_INVALID POLL_ANSWERS_INVALID POLL_OPTION_DUPLICATE POLL_QUESTION_INVALID QUIZ_CORRECT_ANSWERS_EMPTY QUIZ_CORRECT_ANSWERS_TOO_MUCH QUIZ_CORRECT_ANSWER_INVALID QUIZ_MULTIPLE_INVALID RANDOM_ID_DUPLICATE SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH STORAGE_CHECK_FAILED TIMEOUT USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT VIDEO_CONTENT_TYPE_INVALID WEBPAGE_CURL_FAILED WEBPAGE_MEDIA_EMPTY +messages.sendMedia,both,BOT_PAYMENTS_DISABLED BOT_POLLS_DISABLED BROADCAST_PUBLIC_VOTERS_FORBIDDEN CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_SEND_MEDIA_FORBIDDEN CHAT_WRITE_FORBIDDEN CURRENCY_TOTAL_AMOUNT_INVALID EMOTICON_INVALID EXTERNAL_URL_INVALID FILE_PARTS_INVALID FILE_PART_LENGTH_INVALID FILE_REFERENCE_EMPTY FILE_REFERENCE_EXPIRED GAME_BOT_INVALID INPUT_USER_DEACTIVATED MEDIA_CAPTION_TOO_LONG MEDIA_EMPTY PAYMENT_PROVIDER_INVALID PEER_ID_INVALID PHOTO_EXT_INVALID PHOTO_INVALID_DIMENSIONS PHOTO_SAVE_FILE_INVALID POLL_ANSWER_INVALID POLL_ANSWERS_INVALID POLL_OPTION_DUPLICATE POLL_QUESTION_INVALID QUIZ_CORRECT_ANSWERS_EMPTY QUIZ_CORRECT_ANSWERS_TOO_MUCH QUIZ_CORRECT_ANSWER_INVALID QUIZ_MULTIPLE_INVALID RANDOM_ID_DUPLICATE SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH STORAGE_CHECK_FAILED TIMEOUT USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT VIDEO_CONTENT_TYPE_INVALID WEBPAGE_CURL_FAILED WEBPAGE_MEDIA_EMPTY messages.sendMessage,both,AUTH_KEY_DUPLICATED BOT_DOMAIN_INVALID BUTTON_DATA_INVALID BUTTON_TYPE_INVALID BUTTON_URL_INVALID CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_ID_INVALID CHAT_RESTRICTED CHAT_WRITE_FORBIDDEN ENTITIES_TOO_LONG ENTITY_MENTION_USER_INVALID INPUT_USER_DEACTIVATED MESSAGE_EMPTY MESSAGE_TOO_LONG MSG_ID_INVALID PEER_ID_INVALID POLL_OPTION_INVALID RANDOM_ID_DUPLICATE REPLY_MARKUP_INVALID REPLY_MARKUP_TOO_LONG SCHEDULE_BOT_NOT_ALLOWED SCHEDULE_DATE_TOO_LATE SCHEDULE_STATUS_PRIVATE SCHEDULE_TOO_MUCH TIMEOUT USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT YOU_BLOCKED_USER messages.sendMultiMedia,both,MULTI_MEDIA_TOO_LONG SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH messages.sendScheduledMessages,user, -messages.sendVote,user,MESSAGE_POLL_CLOSED OPTION_INVALID +messages.sendVote,user,MESSAGE_POLL_CLOSED OPTIONS_TOO_MUCH OPTION_INVALID REVOTE_NOT_ALLOWED messages.setBotCallbackAnswer,both,QUERY_ID_INVALID URL_INVALID messages.setBotPrecheckoutResults,both,ERROR_TEXT_EMPTY messages.setBotShippingResults,both,QUERY_ID_INVALID messages.setChatTheme,user,EMOJI_INVALID EMOJI_NOT_MODIFIED PEER_ID_INVALID messages.setEncryptedTyping,user,CHAT_ID_INVALID -messages.setGameScore,bot,PEER_ID_INVALID USER_BOT_REQUIRED +messages.setGameScore,bot,BOT_SCORE_NOT_MODIFIED PEER_ID_INVALID SCORE_INVALID USER_BOT_REQUIRED messages.setHistoryTTL,user,CHAT_NOT_MODIFIED TTL_PERIOD_INVALID messages.setInlineBotResults,bot,ARTICLE_TITLE_EMPTY AUDIO_CONTENT_URL_EMPTY AUDIO_TITLE_EMPTY BUTTON_DATA_INVALID BUTTON_TYPE_INVALID BUTTON_URL_INVALID DOCUMENT_INVALID FILE_CONTENT_TYPE_INVALID FILE_TITLE_EMPTY GIF_CONTENT_TYPE_INVALID MESSAGE_EMPTY NEXT_OFFSET_INVALID PHOTO_CONTENT_TYPE_INVALID PHOTO_CONTENT_URL_EMPTY PHOTO_THUMB_URL_EMPTY QUERY_ID_INVALID REPLY_MARKUP_INVALID RESULT_TYPE_INVALID SEND_MESSAGE_MEDIA_INVALID SEND_MESSAGE_TYPE_INVALID START_PARAM_INVALID STICKER_DOCUMENT_INVALID USER_BOT_INVALID VIDEO_TITLE_EMPTY WEBDOCUMENT_MIME_INVALID WEBDOCUMENT_URL_INVALID messages.setInlineGameScore,bot,MESSAGE_ID_INVALID USER_BOT_REQUIRED From 7778e54467d1ed5079828aad2069f4571d39e107 Mon Sep 17 00:00:00 2001 From: Devesh Pal Date: Tue, 25 Jan 2022 01:39:51 +0530 Subject: [PATCH 174/256] Allow restricting channels (#3679) --- telethon/_client/chats.py | 8 ++------ telethon/_events/inlinequery.py | 2 +- telethon/types/_custom/button.py | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/telethon/_client/chats.py b/telethon/_client/chats.py index cd6d9726..8ab4bff6 100644 --- a/telethon/_client/chats.py +++ b/telethon/_client/chats.py @@ -577,9 +577,6 @@ async def edit_permissions( )) user = await self.get_input_entity(user) - ty = helpers._entity_type(user) - if ty != helpers._EntityType.USER: - raise ValueError('You must pass a user entity') if isinstance(user, _tl.InputPeerSelf): raise ValueError('You cannot restrict yourself') @@ -630,7 +627,7 @@ async def get_permissions( self: 'TelegramClient', entity: 'hints.EntityLike', user: 'hints.EntityLike' = None -) -> 'typing.Optional[custom.ParticipantPermissions]': +) -> 'typing.Optional[_custom.ParticipantPermissions]': entity = await self.get_entity(entity) if not user: @@ -639,8 +636,7 @@ async def get_permissions( entity = await self.get_input_entity(entity) user = await self.get_input_entity(user) - if helpers._entity_type(user) != helpers._EntityType.USER: - raise ValueError('You must pass a user entity') + if helpers._entity_type(entity) == helpers._EntityType.CHANNEL: participant = await self(_tl.fn.channels.GetParticipant( entity, diff --git a/telethon/_events/inlinequery.py b/telethon/_events/inlinequery.py index 76401962..59ad6baa 100644 --- a/telethon/_events/inlinequery.py +++ b/telethon/_events/inlinequery.py @@ -137,7 +137,7 @@ class InlineQuery(EventBuilder): Returns a new `InlineBuilder ` instance. """ - return custom.InlineBuilder(self._client) + return _custom.InlineBuilder(self._client) async def answer( self, results=None, cache_time=0, *, diff --git a/telethon/types/_custom/button.py b/telethon/types/_custom/button.py index 27f1aae9..0ea42d40 100644 --- a/telethon/types/_custom/button.py +++ b/telethon/types/_custom/button.py @@ -170,7 +170,7 @@ class Button: ) @staticmethod - def mention(text, input_entity): + def mention(text, input_entity=None): """ Creates a new inline button linked to the profile of user. From d426099bf5304c6736f7cc5a5c6274cbdd39ad09 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 26 Jan 2022 11:03:50 +0100 Subject: [PATCH 175/256] Remove input_peer parameter from get_me --- readthedocs/misc/v2-migration-guide.rst | 2 ++ telethon/_client/chats.py | 2 +- telethon/_client/telegramclient.py | 13 ++----------- telethon/_client/users.py | 7 +++---- telethon/_events/common.py | 2 +- 5 files changed, 9 insertions(+), 17 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index dc751b1a..3e4905b1 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -773,3 +773,5 @@ connection type is gone raise_last_call_error is now the default rather than ValueError self-produced updates like getmessage now also trigger a handler + +input_peer removed from get_me; input peers should remain mostly an impl detail diff --git a/telethon/_client/chats.py b/telethon/_client/chats.py index 8ab4bff6..e8a95291 100644 --- a/telethon/_client/chats.py +++ b/telethon/_client/chats.py @@ -648,7 +648,7 @@ async def get_permissions( entity )) if isinstance(user, _tl.InputPeerSelf): - user = await self.get_me(input_peer=True) + user = _tl.PeerUser(self._session_state.user_id) for participant in chat.full_chat.participants.participants: if participant.user_id == user.user_id: return _custom.ParticipantPermissions(participant, True) diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index d78b0ce6..3196c1eb 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -3274,19 +3274,13 @@ class TelegramClient: """ @forward_call(users.get_me) - async def get_me(self: 'TelegramClient', input_peer: bool = False) \ - -> 'typing.Union[_tl.User, _tl.InputPeerUser]': + async def get_me(self: 'TelegramClient') \ + -> '_tl.User': """ Gets "me", the current :tl:`User` who is logged in. If the user has not logged in yet, this method returns `None`. - Arguments - input_peer (`bool`, optional): - Whether to return the :tl:`InputPeerUser` version or the normal - :tl:`User`. This can be useful if you just need to know the ID - of yourself. - Returns Your own :tl:`User`. @@ -3434,9 +3428,6 @@ class TelegramClient: :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. - Example .. code-block:: python diff --git a/telethon/_client/users.py b/telethon/_client/users.py index 53da7574..05f925bd 100644 --- a/telethon/_client/users.py +++ b/telethon/_client/users.py @@ -126,11 +126,10 @@ async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sl raise last_error -async def get_me(self: 'TelegramClient', input_peer: bool = False) \ +async def get_me(self: 'TelegramClient') \ -> 'typing.Union[_tl.User, _tl.InputPeerUser]': try: - me = (await self(_tl.fn.users.GetUsers([_tl.InputUserSelf()])))[0] - return utils.get_input_peer(me, allow_self=False) if input_peer else me + return (await self(_tl.fn.users.GetUsers([_tl.InputUserSelf()])))[0] except UnauthorizedError: return None @@ -296,7 +295,7 @@ async def get_peer_id( peer = await self.get_input_entity(peer) if isinstance(peer, _tl.InputPeerSelf): - peer = await self.get_me(input_peer=True) + peer = _tl.PeerUser(self._session_state.user_id) return utils.get_peer_id(peer) diff --git a/telethon/_events/common.py b/telethon/_events/common.py index fb941980..c20ac64e 100644 --- a/telethon/_events/common.py +++ b/telethon/_events/common.py @@ -25,7 +25,7 @@ async def _into_id_set(client, chats): else: chat = await client.get_input_entity(chat) if isinstance(chat, _tl.InputPeerSelf): - chat = await client.get_me(input_peer=True) + chat = _tl.PeerUser(self._session_state.user_id) result.add(utils.get_peer_id(chat)) return result From 070af28e855535c48865da23a4ad30288bdc0270 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 26 Jan 2022 12:14:17 +0100 Subject: [PATCH 176/256] Make raw API types immutable --- readthedocs/misc/v2-migration-guide.rst | 2 + telethon/_client/telegrambaseclient.py | 16 ++- telethon/_client/users.py | 6 +- telethon/_misc/tlobject.py | 2 +- telethon_generator/generators/tlobject.py | 139 ++++++++++++---------- 5 files changed, 90 insertions(+), 75 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 3e4905b1..337377cb 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -775,3 +775,5 @@ raise_last_call_error is now the default rather than ValueError self-produced updates like getmessage now also trigger a handler input_peer removed from get_me; input peers should remain mostly an impl detail + +raw api types and fns are now immutable. this can enable optimizations in the future. diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 480263e4..92dc1b65 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -8,6 +8,7 @@ import time import typing import ipaddress import dataclasses +import functools from .. import version, __name__ as __base_name__, _tl from .._crypto import rsa @@ -182,7 +183,8 @@ def init( default_device_model = system.machine default_system_version = re.sub(r'-.+','',system.release) - self._init_request = _tl.fn.InitConnection( + self._init_request = functools.partial( + _tl.fn.InitConnection, api_id=self._api_id, device_model=device_model or default_device_model or 'Unknown', system_version=system_version or default_system_version or '1.0', @@ -190,8 +192,6 @@ def init( lang_code=lang_code, system_lang_code=system_lang_code, lang_pack='', # "langPacks are for official apps only" - query=None, - proxy=None ) self._sender = MTProtoSender( @@ -272,10 +272,8 @@ async def connect(self: 'TelegramClient') -> None: # Need to send invokeWithLayer for things to work out. # Make the most out of this opportunity by also refreshing our state. # During the v1 to v2 migration, this also correctly sets the IPv* columns. - self._init_request.query = _tl.fn.help.GetConfig() - config = await self._sender.send(_tl.fn.InvokeWithLayer( - _tl.LAYER, self._init_request + _tl.LAYER, self._init_request(query=_tl.fn.help.GetConfig()) )) for dc in config.dc_options: @@ -318,7 +316,6 @@ async def disconnect(self: 'TelegramClient'): def set_proxy(self: 'TelegramClient', proxy: typing.Union[tuple, dict]): init_proxy = None - self._init_request.proxy = init_proxy self._proxy = proxy # While `await client.connect()` passes new proxy on each new call, @@ -408,8 +405,9 @@ async def _create_exported_sender(self: 'TelegramClient', dc_id): )) self._log[__name__].info('Exporting auth for new borrowed sender in %s', dc) auth = await self(_tl.fn.auth.ExportAuthorization(dc_id)) - self._init_request.query = _tl.fn.auth.ImportAuthorization(id=auth.id, bytes=auth.bytes) - req = _tl.fn.InvokeWithLayer(_tl.LAYER, self._init_request) + req = _tl.fn.InvokeWithLayer(_tl.LAYER, self._init_request( + query=_tl.fn.auth.ImportAuthorization(id=auth.id, bytes=auth.bytes) + )) await sender.send(req) return sender diff --git a/telethon/_client/users.py b/telethon/_client/users.py index 05f925bd..763a31c4 100644 --- a/telethon/_client/users.py +++ b/telethon/_client/users.py @@ -35,10 +35,11 @@ async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sl if flood_sleep_threshold is None: flood_sleep_threshold = self.flood_sleep_threshold requests = (request if utils.is_list_like(request) else (request,)) + new_requests = [] for r in requests: if not isinstance(r, _tl.TLRequest): raise _NOT_A_REQUEST() - await r.resolve(self, utils) + r = await r.resolve(self, utils) # Avoid making the request if it's already in a flood wait if r.CONSTRUCTOR_ID in self._flood_waited_requests: @@ -59,6 +60,9 @@ async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sl if self._no_updates: r = _tl.fn.InvokeWithoutUpdates(r) + new_requests.append(r) + request = new_requests if utils.is_list_like(request) else new_requests[0] + request_index = 0 last_error = None self._last_request = time.time() diff --git a/telethon/_misc/tlobject.py b/telethon/_misc/tlobject.py index 6b4bddf8..397dc1b0 100644 --- a/telethon/_misc/tlobject.py +++ b/telethon/_misc/tlobject.py @@ -155,4 +155,4 @@ class TLRequest(TLObject): return reader.tgread_object() async def resolve(self, client, utils): - pass + return self diff --git a/telethon_generator/generators/tlobject.py b/telethon_generator/generators/tlobject.py index c1e5e14a..3c0959c1 100644 --- a/telethon_generator/generators/tlobject.py +++ b/telethon_generator/generators/tlobject.py @@ -85,6 +85,9 @@ def _write_modules( # Import struct for the .__bytes__(self) serialization builder.writeln('import struct') + # Import dataclasses in order to freeze the instances + builder.writeln('import dataclasses') + # Import datetime for type hinting builder.writeln('from datetime import datetime') @@ -187,37 +190,9 @@ def _write_source_code(tlobject, kind, builder, type_constructors): def _write_class_init(tlobject, kind, type_constructors, builder): builder.writeln() builder.writeln() + builder.writeln('@dataclasses.dataclass(init=False, frozen=True)') builder.writeln('class {}({}):', tlobject.class_name, kind) - # Define slots to help reduce the size of the objects a little bit. - # It's also good for knowing what fields an object has. - builder.write('__slots__ = (') - sep = '' - for arg in tlobject.real_args: - builder.write('{}{!r},', sep, arg.name) - sep = ' ' - builder.writeln(')') - - # Class-level variable to store its Telegram's constructor ID - builder.writeln('CONSTRUCTOR_ID = {:#x}', tlobject.id) - builder.writeln('SUBCLASS_OF_ID = {:#x}', - crc32(tlobject.result.encode('ascii'))) - builder.writeln() - - # Convert the args to string parameters, flags having =None - args = ['{}: {}{}'.format( - a.name, a.type_hint(), '=None' if a.is_flag or a.can_be_inferred else '') - for a in tlobject.real_args - ] - - # Write the __init__ function if it has any argument - if not tlobject.real_args: - return - - if any(a.name in dir(builtins) for a in tlobject.real_args): - builder.writeln('# noinspection PyShadowingBuiltins') - - builder.writeln("def __init__({}):", ', '.join(['self'] + args)) builder.writeln('"""') if tlobject.is_function: builder.write(':returns {}: ', tlobject.result) @@ -236,47 +211,83 @@ def _write_class_init(tlobject, kind, type_constructors, builder): builder.writeln('"""') - # Set the arguments + # Define slots to help reduce the size of the objects a little bit. + # It's also good for knowing what fields an object has. + builder.write('__slots__ = (') + sep = '' for arg in tlobject.real_args: - if not arg.can_be_inferred: - builder.writeln('self.{0} = {0}', arg.name) + builder.write('{}{!r},', sep, arg.name) + sep = ' ' + builder.writeln(')') - # Currently the only argument that can be - # inferred are those called 'random_id' - elif arg.name == 'random_id': - # Endianness doesn't really matter, and 'big' is shorter - code = "int.from_bytes(os.urandom({}), 'big', signed=True)" \ - .format(8 if arg.type == 'long' else 4) + # Class-level variable to store its Telegram's constructor ID + builder.writeln('CONSTRUCTOR_ID = {:#x}', tlobject.id) + builder.writeln('SUBCLASS_OF_ID = {:#x}', + crc32(tlobject.result.encode('ascii'))) + builder.writeln() - if arg.is_vector: - # Currently for the case of "messages.forwardMessages" - # Ensure we can infer the length from id:Vector<> - if not next(a for a in tlobject.real_args - if a.name == 'id').is_vector: - raise ValueError( - 'Cannot infer list of random ids for ', tlobject - ) - code = '[{} for _ in range(len(id))]'.format(code) + # Because we're using __slots__ and frozen instances, we cannot have flags = None directly. + # See https://stackoverflow.com/q/50180735 (Python 3.10 does offer a solution). + # Write the __init__ function if it has any argument. + if tlobject.real_args: + # Convert the args to string parameters + for a in tlobject.real_args: + builder.writeln('{}: {}', a.name, a.type_hint()) - builder.writeln( - "self.random_id = random_id if random_id " - "is not None else {}", code - ) - else: - raise ValueError('Cannot infer a value for ', arg) + # Convert the args to string parameters, flags having =None + args = ['{}: {}{}'.format( + a.name, a.type_hint(), '=None' if a.is_flag or a.can_be_inferred else '') + for a in tlobject.real_args + ] - builder.end_block() + if any(a.name in dir(builtins) for a in tlobject.real_args): + builder.writeln('# noinspection PyShadowingBuiltins') + + builder.writeln("def __init__({}):", ', '.join(['self'] + args)) + + # Set the arguments + for arg in tlobject.real_args: + builder.writeln("object.__setattr__(self, '{0}', {0})", arg.name) + + builder.end_block() def _write_resolve(tlobject, builder): if tlobject.is_function and any( - (arg.type in AUTO_CASTS - or ((arg.name, arg.type) in NAMED_AUTO_CASTS - and tlobject.fullname not in NAMED_BLACKLIST)) - for arg in tlobject.real_args + (arg.can_be_inferred + or arg.type in AUTO_CASTS + or ((arg.name, arg.type) in NAMED_AUTO_CASTS and tlobject.fullname not in NAMED_BLACKLIST)) + for arg in tlobject.real_args ): builder.writeln('async def resolve(self, client, utils):') + builder.writeln('r = {}') # hold replacements + for arg in tlobject.real_args: + if arg.can_be_inferred: + builder.writeln('if self.{} is None:', arg.name) + + # Currently the only argument that can be + # inferred are those called 'random_id' + if arg.name == 'random_id': + # Endianness doesn't really matter, and 'big' is shorter + code = "int.from_bytes(os.urandom({}), 'big', signed=True)" \ + .format(8 if arg.type == 'long' else 4) + + if arg.is_vector: + # Currently for the case of "messages.forwardMessages" + # Ensure we can infer the length from id:Vector<> + if not next(a for a in tlobject.real_args if a.name == 'id').is_vector: + raise ValueError('Cannot infer list of random ids for ', tlobject) + + code = '[{} for _ in range(len(self.id))]'.format(code) + + builder.writeln("r['{}'] = {}", arg.name, code) + else: + raise ValueError('Cannot infer a value for ', arg) + + builder.end_block() + continue + ac = AUTO_CASTS.get(arg.type) if not ac: ac = NAMED_AUTO_CASTS.get((arg.name, arg.type)) @@ -287,17 +298,17 @@ def _write_resolve(tlobject, builder): builder.writeln('if self.{}:', arg.name) if arg.is_vector: - builder.writeln('_tmp = []') - builder.writeln('for _x in self.{0}:', arg.name) - builder.writeln('_tmp.append({})', ac.format('_x')) + builder.writeln("r['{}'] = []", arg.name) + builder.writeln('for x in self.{0}:', arg.name) + builder.writeln("r['{}'].append({})", arg.name, ac.format('x')) builder.end_block() - builder.writeln('self.{} = _tmp', arg.name) else: - builder.writeln('self.{} = {}', arg.name, - ac.format('self.' + arg.name)) + builder.writeln("r['{}'] = {}", arg.name, ac.format('self.' + arg.name)) if arg.is_flag: builder.end_block() + + builder.writeln('return dataclasses.replace(self, **r)') builder.end_block() From f2ef0bfcebd372e8e8c9655cd45d79ad5c27f453 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 26 Jan 2022 12:24:45 +0100 Subject: [PATCH 177/256] Make upload_file private --- readthedocs/misc/v2-migration-guide.rst | 2 + telethon/_client/messages.py | 4 +- telethon/_client/telegramclient.py | 103 +++---------------- telethon/_client/uploads.py | 74 +++++++++++++ telethon_generator/data/friendly.csv | 1 - telethon_generator/parsers/tlobject/tlarg.py | 6 +- 6 files changed, 95 insertions(+), 95 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 337377cb..79681ff0 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -777,3 +777,5 @@ self-produced updates like getmessage now also trigger a handler input_peer removed from get_me; input peers should remain mostly an impl detail raw api types and fns are now immutable. this can enable optimizations in the future. + +upload_file has been removed from the public methods. it's a low-level method users should not need to use. diff --git a/telethon/_client/messages.py b/telethon/_client/messages.py index 250582d0..0997a643 100644 --- a/telethon/_client/messages.py +++ b/telethon/_client/messages.py @@ -476,10 +476,10 @@ async def send_message( # TODO album if message._file._should_upload_thumb(): - message._file._set_uploaded_thumb(await self.upload_file(message._file._thumb)) + message._file._set_uploaded_thumb(await self._upload_file(message._file._thumb)) if message._file._should_upload_file(): - message._file._set_uploaded_file(await self.upload_file(message._file._file)) + message._file._set_uploaded_file(await self._upload_file(message._file._file)) request = _tl.fn.messages.SendMedia( entity, message._file._media, reply_to_msg_id=reply_to, message=message._text, diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 3196c1eb..67141622 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -2983,8 +2983,6 @@ class TelegramClient: message with media before, you can use its ``message.media`` as a file here). - * A handle to an uploaded file (from `upload_file`). - * A :tl:`InputMedia` instance. For example, if you want to send a dice use :tl:`InputMediaDice`, or if you want to send a contact use :tl:`InputMediaContact`. @@ -3157,93 +3155,6 @@ class TelegramClient: )) """ - @forward_call(uploads.upload_file) - async def upload_file( - self: 'TelegramClient', - file: 'hints.FileLike', - *, - part_size_kb: float = None, - file_size: int = None, - file_name: str = None, - use_cache: type = None, - key: bytes = None, - iv: bytes = None, - progress_callback: 'hints.ProgressCallback' = None) -> '_tl.TypeInputFile': - """ - Uploads a file to Telegram's servers, without sending it. - - .. note:: - - Generally, you want to use `send_file` instead. - - This method returns a handle (an instance of :tl:`InputFile` or - :tl:`InputFileBig`, as required) which can be later used before - it expires (they are usable during less than a day). - - Uploading a file will simply return a "handle" to the file stored - remotely in the Telegram servers, which can be later used on. This - will **not** upload the file to your own chat or any chat at all. - - Arguments - file (`str` | `bytes` | `file`): - The path of the file, byte array, or stream that will be sent. - Note that if a byte array or a stream is given, a filename - or its type won't be inferred, and it will be sent as an - "unnamed application/octet-stream". - - part_size_kb (`int`, optional): - Chunk size when uploading files. The larger, the less - requests will be made (up to 512KB maximum). - - file_size (`int`, optional): - The size of the file to be uploaded, which will be determined - automatically if not specified. - - If the file size can't be determined beforehand, the entire - file will be read in-memory to find out how large it is. - - file_name (`str`, optional): - The file name which will be used on the resulting InputFile. - If not specified, the name will be taken from the ``file`` - and if this is not a `str`, it will be ``"unnamed"``. - - use_cache (`type`, optional): - This parameter currently does nothing, but is kept for - backward-compatibility (and it may get its use back in - the future). - - key ('bytes', optional): - In case of an encrypted upload (secret chats) a key is supplied - - iv ('bytes', optional): - In case of an encrypted upload (secret chats) an iv is supplied - - progress_callback (`callable`, optional): - A callback function accepting two parameters: - ``(sent bytes, total)``. - - Returns - :tl:`InputFileBig` if the file size is larger than 10MB, - `InputSizedFile ` - (subclass of :tl:`InputFile`) otherwise. - - Example - .. code-block:: python - - # Photos as photo and document - file = await client.upload_file('photo.jpg') - await client.send_file(chat, file) # sends as photo - await client.send_file(chat, file, force_document=True) # sends as document - - file.name = 'not a photo.jpg' - await client.send_file(chat, file, force_document=True) # document, new name - - # As song or as voice note - file = await client.upload_file('song.ogg') - await client.send_file(chat, file) # sends as song - await client.send_file(chat, file, voice_note=True) # sends as voice note - """ - # endregion Uploads # region Users @@ -3508,4 +3419,18 @@ class TelegramClient: async def _replace_session_state(self, *, save=True, **changes): pass + @forward_call(uploads.upload_file) + async def _upload_file( + self: 'TelegramClient', + file: 'hints.FileLike', + *, + part_size_kb: float = None, + file_size: int = None, + file_name: str = None, + use_cache: type = None, + key: bytes = None, + iv: bytes = None, + progress_callback: 'hints.ProgressCallback' = None) -> '_tl.TypeInputFile': + pass + # endregion Private diff --git a/telethon/_client/uploads.py b/telethon/_client/uploads.py index a92df8f4..218e6351 100644 --- a/telethon/_client/uploads.py +++ b/telethon/_client/uploads.py @@ -235,6 +235,80 @@ async def upload_file( key: bytes = None, iv: bytes = None, progress_callback: 'hints.ProgressCallback' = None) -> '_tl.TypeInputFile': + """ + Uploads a file to Telegram's servers, without sending it. + + .. note:: + + Generally, you want to use `send_file` instead. + + This method returns a handle (an instance of :tl:`InputFile` or + :tl:`InputFileBig`, as required) which can be later used before + it expires (they are usable during less than a day). + + Uploading a file will simply return a "handle" to the file stored + remotely in the Telegram servers, which can be later used on. This + will **not** upload the file to your own chat or any chat at all. + + Arguments + file (`str` | `bytes` | `file`): + The path of the file, byte array, or stream that will be sent. + Note that if a byte array or a stream is given, a filename + or its type won't be inferred, and it will be sent as an + "unnamed application/octet-stream". + + part_size_kb (`int`, optional): + Chunk size when uploading files. The larger, the less + requests will be made (up to 512KB maximum). + + file_size (`int`, optional): + The size of the file to be uploaded, which will be determined + automatically if not specified. + + If the file size can't be determined beforehand, the entire + file will be read in-memory to find out how large it is. + + file_name (`str`, optional): + The file name which will be used on the resulting InputFile. + If not specified, the name will be taken from the ``file`` + and if this is not a `str`, it will be ``"unnamed"``. + + use_cache (`type`, optional): + This parameter currently does nothing, but is kept for + backward-compatibility (and it may get its use back in + the future). + + key ('bytes', optional): + In case of an encrypted upload (secret chats) a key is supplied + + iv ('bytes', optional): + In case of an encrypted upload (secret chats) an iv is supplied + + progress_callback (`callable`, optional): + A callback function accepting two parameters: + ``(sent bytes, total)``. + + Returns + :tl:`InputFileBig` if the file size is larger than 10MB, + `InputSizedFile ` + (subclass of :tl:`InputFile`) otherwise. + + Example + .. code-block:: python + + # Photos as photo and document + file = await client.upload_file('photo.jpg') + await client.send_file(chat, file) # sends as photo + await client.send_file(chat, file, force_document=True) # sends as document + + file.name = 'not a photo.jpg' + await client.send_file(chat, file, force_document=True) # document, new name + + # As song or as voice note + file = await client.upload_file('song.ogg') + await client.send_file(chat, file) # sends as song + await client.send_file(chat, file, voice_note=True) # sends as voice note + """ if isinstance(file, (_tl.InputFile, _tl.InputFileBig)): return file # Already uploaded diff --git a/telethon_generator/data/friendly.csv b/telethon_generator/data/friendly.csv index 950a8bd7..4d53c862 100644 --- a/telethon_generator/data/friendly.csv +++ b/telethon_generator/data/friendly.csv @@ -23,5 +23,4 @@ messages.MessageMethods,delete_messages,channels.deleteMessages messages.deleteM messages.MessageMethods,send_read_acknowledge,messages.readMentions channels.readHistory messages.readHistory updates.UpdateMethods,catch_up,updates.getDifference updates.getChannelDifference uploads.UploadMethods,send_file,messages.sendMedia messages.sendMultiMedia messages.uploadMedia -uploads.UploadMethods,upload_file,upload.saveFilePart upload.saveBigFilePart users.UserMethods,get_entity,users.getUsers messages.getChats channels.getChannels contacts.resolveUsername diff --git a/telethon_generator/parsers/tlobject/tlarg.py b/telethon_generator/parsers/tlobject/tlarg.py index 4a86fca9..0c4c7464 100644 --- a/telethon_generator/parsers/tlobject/tlarg.py +++ b/telethon_generator/parsers/tlobject/tlarg.py @@ -37,7 +37,7 @@ KNOWN_NAMED_EXAMPLES = { ('lang_code', 'string'): "'en'", ('chat_id', 'int'): '478614198', ('client_id', 'long'): 'random.randrange(-2**63, 2**63)', - ('video', 'InputFile'): "client.upload_file('/path/to/file.mp4')", + ('video', 'InputFile'): "client._upload_file('/path/to/file.mp4')", } KNOWN_TYPED_EXAMPLES = { @@ -50,8 +50,8 @@ KNOWN_TYPED_EXAMPLES = { 'double': '7.13', 'Bool': 'False', 'true': 'True', - 'InputChatPhoto': "client.upload_file('/path/to/photo.jpg')", - 'InputFile': "client.upload_file('/path/to/file.jpg')", + 'InputChatPhoto': "client._upload_file('/path/to/photo.jpg')", + 'InputFile': "client._upload_file('/path/to/file.jpg')", 'InputPeer': "'username'" } From 9726169a8c162acf01a03d7755d03a9243c1a81a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 28 Jan 2022 11:34:16 +0100 Subject: [PATCH 178/256] Begin unification of event builders and events --- readthedocs/misc/v2-migration-guide.rst | 9 + telethon/_client/telegramclient.py | 2 +- telethon/_client/updates.py | 2 +- telethon/_events/album.py | 363 +++++++-------- telethon/_events/base.py | 112 +---- telethon/_events/callbackquery.py | 427 ++++++++--------- telethon/_events/chataction.py | 578 ++++++++++++------------ telethon/_events/common.py | 179 -------- telethon/_events/inlinequery.py | 330 ++++++-------- telethon/_events/messagedeleted.py | 21 +- telethon/_events/messageedited.py | 11 +- telethon/_events/messageread.py | 160 +++---- telethon/_events/newmessage.py | 181 ++------ telethon/_events/raw.py | 34 +- telethon/_events/userupdate.py | 452 +++++++++--------- 15 files changed, 1142 insertions(+), 1719 deletions(-) delete mode 100644 telethon/_events/common.py diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 79681ff0..ce077d13 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -779,3 +779,12 @@ input_peer removed from get_me; input peers should remain mostly an impl detail raw api types and fns are now immutable. this can enable optimizations in the future. upload_file has been removed from the public methods. it's a low-level method users should not need to use. + +events have changed. rather than differentiating between "event builder" and "event instance", instead there is only the instance, and you register the class. +where you had +@client.on(events.NewMessage(chats=...)) +it's now +@client.on(events.NewMessage, chats=...) +this also means filters are unified, although not all have an effect on all events. from_users renamed to senders. messageread inbox is gone in favor of outgoing/incoming. +events.register, unregister, is_handler and list are gone. now you can typehint instead. +def handler(event: events.NewMessage) diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 67141622..136a6d31 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -10,7 +10,7 @@ from . import ( ) from .. import version, _tl from ..types import _custom -from .._events.common import EventBuilder, EventCommon +from .._events.base import EventBuilder from .._misc import enums diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index cf26e809..a9875bba 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -10,7 +10,7 @@ import logging from collections import deque from ..errors._rpcbase import RpcError -from .._events.common import EventBuilder, EventCommon +from .._events.base import EventBuilder from .._events.raw import Raw from .._events.base import StopPropagation, _get_handlers from .._misc import utils diff --git a/telethon/_events/album.py b/telethon/_events/album.py index 580e5a31..41646acf 100644 --- a/telethon/_events/album.py +++ b/telethon/_events/album.py @@ -2,7 +2,7 @@ import asyncio import time import weakref -from .common import EventBuilder, EventCommon, name_inner_event +from .base import EventBuilder from .._misc import utils from .. import _tl from ..types import _custom @@ -64,13 +64,16 @@ class AlbumHack: await asyncio.sleep(diff) -@name_inner_event -class Album(EventBuilder): +class Album(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.SenderGetter): """ Occurs whenever you receive an album. This event only exists to ease dealing with an unknown amount of messages that belong to the same album. + Members: + messages (Sequence[`Message `]): + The list of messages belonging to the same album. + Example .. code-block:: python @@ -91,12 +94,20 @@ class Album(EventBuilder): await event.messages[4].reply('Cool!') """ - def __init__( - self, chats=None, *, blacklist_chats=False, func=None): - super().__init__(chats, blacklist_chats=blacklist_chats, func=func) + def __init__(self, messages): + message = messages[0] + if not message.out and isinstance(message.peer_id, _tl.PeerUser): + # Incoming message (e.g. from a bot) has peer_id=us, and + # from_id=bot (the actual "chat" from a user's perspective). + chat_peer = message.from_id + else: + chat_peer = message.peer_id - @classmethod - def build(cls, update, others=None, self_id=None, *todo, **todo2): + _custom.chatgetter.ChatGetter.__init__(self, chat_peer=chat_peer, broadcast=bool(message.post)) + _custom.sendergetter.SenderGetter.__init__(self, message.sender_id) + self.messages = messages + + def _build(cls, update, others=None, self_id=None, *todo, **todo2): if not others: return # We only care about albums which come inside the same Updates @@ -135,216 +146,188 @@ class Album(EventBuilder): and u.message.grouped_id == group) ]) - def filter(self, event): - # Albums with less than two messages require a few hacks to work. - if len(event.messages) > 1: - return super().filter(event) + def _set_client(self, client): + super()._set_client(client) + self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities) - class Event(EventCommon, _custom.sendergetter.SenderGetter): - """ - Represents the event of a new album. + self.messages = [ + _custom.Message._new(client, m, self._entities, None) + for m in self.messages + ] - Members: - messages (Sequence[`Message `]): - The list of messages belonging to the same album. - """ - def __init__(self, messages): - message = messages[0] - if not message.out and isinstance(message.peer_id, _tl.PeerUser): - # Incoming message (e.g. from a bot) has peer_id=us, and - # from_id=bot (the actual "chat" from a user's perspective). - chat_peer = message.from_id + if len(self.messages) == 1: + # This will require hacks to be a proper album event + hack = client._albums.get(self.grouped_id) + if hack is None: + client._albums[self.grouped_id] = AlbumHack(client, self) else: - chat_peer = message.peer_id + hack.extend(self.messages) - super().__init__(chat_peer=chat_peer, - msg_id=message.id, broadcast=bool(message.post)) + @property + def grouped_id(self): + """ + The shared ``grouped_id`` between all the messages. + """ + return self.messages[0].grouped_id - _custom.sendergetter.SenderGetter.__init__(self, message.sender_id) - self.messages = messages + @property + def text(self): + """ + The message text of the first photo with a caption, + formatted using the client's default parse mode. + """ + return next((m.text for m in self.messages if m.text), '') - def _set_client(self, client): - super()._set_client(client) - self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities) + @property + def raw_text(self): + """ + The raw message text of the first photo + with a caption, ignoring any formatting. + """ + return next((m.raw_text for m in self.messages if m.raw_text), '') - self.messages = [ - _custom.Message._new(client, m, self._entities, None) - for m in self.messages - ] + @property + def is_reply(self): + """ + `True` if the album is a reply to some other message. - if len(self.messages) == 1: - # This will require hacks to be a proper album event - hack = client._albums.get(self.grouped_id) - if hack is None: - client._albums[self.grouped_id] = AlbumHack(client, self) - else: - hack.extend(self.messages) + Remember that you can access the ID of the message + this one is replying to through `reply_to_msg_id`, + and the `Message` object with `get_reply_message()`. + """ + # Each individual message in an album all reply to the same message + return self.messages[0].is_reply - @property - def grouped_id(self): - """ - The shared ``grouped_id`` between all the messages. - """ - return self.messages[0].grouped_id + @property + def forward(self): + """ + The `Forward ` + information for the first message in the album if it was forwarded. + """ + # Each individual message in an album all reply to the same message + return self.messages[0].forward - @property - def text(self): - """ - The message text of the first photo with a caption, - formatted using the client's default parse mode. - """ - return next((m.text for m in self.messages if m.text), '') + # endregion Public Properties - @property - def raw_text(self): - """ - The raw message text of the first photo - with a caption, ignoring any formatting. - """ - return next((m.raw_text for m in self.messages if m.raw_text), '') + # region Public Methods - @property - def is_reply(self): - """ - `True` if the album is a reply to some other message. + async def get_reply_message(self): + """ + The `Message ` + that this album is replying to, or `None`. - Remember that you can access the ID of the message - this one is replying to through `reply_to_msg_id`, - and the `Message` object with `get_reply_message()`. - """ - # Each individual message in an album all reply to the same message - return self.messages[0].is_reply + The result will be cached after its first use. + """ + return await self.messages[0].get_reply_message() - @property - def forward(self): - """ - The `Forward ` - information for the first message in the album if it was forwarded. - """ - # Each individual message in an album all reply to the same message - return self.messages[0].forward + async def respond(self, *args, **kwargs): + """ + Responds to the album (not as a reply). Shorthand for + `telethon.client.messages.MessageMethods.send_message` + with ``entity`` already set. + """ + return await self.messages[0].respond(*args, **kwargs) - # endregion Public Properties + async def reply(self, *args, **kwargs): + """ + Replies to the first photo in the album (as a reply). Shorthand + for `telethon.client.messages.MessageMethods.send_message` + with both ``entity`` and ``reply_to`` already set. + """ + return await self.messages[0].reply(*args, **kwargs) - # region Public Methods + async def forward_to(self, *args, **kwargs): + """ + Forwards the entire album. Shorthand for + `telethon.client.messages.MessageMethods.forward_messages` + with both ``messages`` and ``from_peer`` already set. + """ + if self._client: + kwargs['messages'] = self.messages + kwargs['from_peer'] = await self.get_input_chat() + return await self._client.forward_messages(*args, **kwargs) - async def get_reply_message(self): - """ - The `Message ` - that this album is replying to, or `None`. + async def edit(self, *args, **kwargs): + """ + Edits the first caption or the message, or the first messages' + caption if no caption is set, iff it's outgoing. Shorthand for + `telethon.client.messages.MessageMethods.edit_message` + with both ``entity`` and ``message`` already set. - The result will be cached after its first use. - """ - return await self.messages[0].get_reply_message() + Returns `None` if the message was incoming, + or the edited `Message` otherwise. - async def respond(self, *args, **kwargs): - """ - Responds to the album (not as a reply). Shorthand for - `telethon.client.messages.MessageMethods.send_message` - with ``entity`` already set. - """ - return await self.messages[0].respond(*args, **kwargs) + .. note:: - async def reply(self, *args, **kwargs): - """ - Replies to the first photo in the album (as a reply). Shorthand - for `telethon.client.messages.MessageMethods.send_message` - with both ``entity`` and ``reply_to`` already set. - """ - return await self.messages[0].reply(*args, **kwargs) + This is different from `client.edit_message + ` + and **will respect** the previous state of the message. + For example, if the message didn't have a link preview, + the edit won't add one by default, and you should force + it by setting it to `True` if you want it. - async def forward_to(self, *args, **kwargs): - """ - Forwards the entire album. Shorthand for - `telethon.client.messages.MessageMethods.forward_messages` - with both ``messages`` and ``from_peer`` already set. - """ - if self._client: - kwargs['messages'] = self.messages - kwargs['from_peer'] = await self.get_input_chat() - return await self._client.forward_messages(*args, **kwargs) + This is generally the most desired and convenient behaviour, + and will work for link previews and message buttons. + """ + for msg in self.messages: + if msg.raw_text: + return await msg.edit(*args, **kwargs) - async def edit(self, *args, **kwargs): - """ - Edits the first caption or the message, or the first messages' - caption if no caption is set, iff it's outgoing. Shorthand for - `telethon.client.messages.MessageMethods.edit_message` - with both ``entity`` and ``message`` already set. + return await self.messages[0].edit(*args, **kwargs) - Returns `None` if the message was incoming, - or the edited `Message` otherwise. + async def delete(self, *args, **kwargs): + """ + Deletes the entire album. You're responsible for checking whether + you have the permission to do so, or to except the error otherwise. + Shorthand for + `telethon.client.messages.MessageMethods.delete_messages` with + ``entity`` and ``message_ids`` already set. + """ + if self._client: + return await self._client.delete_messages( + await self.get_input_chat(), self.messages, + *args, **kwargs + ) - .. note:: + async def mark_read(self): + """ + Marks the entire album as read. Shorthand for + `client.mark_read() + ` + with both ``entity`` and ``message`` already set. + """ + if self._client: + await self._client.mark_read( + await self.get_input_chat(), max_id=self.messages[-1].id) - This is different from `client.edit_message - ` - and **will respect** the previous state of the message. - For example, if the message didn't have a link preview, - the edit won't add one by default, and you should force - it by setting it to `True` if you want it. + async def pin(self, *, notify=False): + """ + Pins the first photo in the album. Shorthand for + `telethon.client.messages.MessageMethods.pin_message` + with both ``entity`` and ``message`` already set. + """ + return await self.messages[0].pin(notify=notify) - This is generally the most desired and convenient behaviour, - and will work for link previews and message buttons. - """ - for msg in self.messages: - if msg.raw_text: - return await msg.edit(*args, **kwargs) + def __len__(self): + """ + Return the amount of messages in the album. - return await self.messages[0].edit(*args, **kwargs) + Equivalent to ``len(self.messages)``. + """ + return len(self.messages) - async def delete(self, *args, **kwargs): - """ - Deletes the entire album. You're responsible for checking whether - you have the permission to do so, or to except the error otherwise. - Shorthand for - `telethon.client.messages.MessageMethods.delete_messages` with - ``entity`` and ``message_ids`` already set. - """ - if self._client: - return await self._client.delete_messages( - await self.get_input_chat(), self.messages, - *args, **kwargs - ) + def __iter__(self): + """ + Iterate over the messages in the album. - async def mark_read(self): - """ - Marks the entire album as read. Shorthand for - `client.mark_read() - ` - with both ``entity`` and ``message`` already set. - """ - if self._client: - await self._client.mark_read( - await self.get_input_chat(), max_id=self.messages[-1].id) + Equivalent to ``iter(self.messages)``. + """ + return iter(self.messages) - async def pin(self, *, notify=False): - """ - Pins the first photo in the album. Shorthand for - `telethon.client.messages.MessageMethods.pin_message` - with both ``entity`` and ``message`` already set. - """ - return await self.messages[0].pin(notify=notify) + def __getitem__(self, n): + """ + Access the n'th message in the album. - def __len__(self): - """ - Return the amount of messages in the album. - - Equivalent to ``len(self.messages)``. - """ - return len(self.messages) - - def __iter__(self): - """ - Iterate over the messages in the album. - - Equivalent to ``iter(self.messages)``. - """ - return iter(self.messages) - - def __getitem__(self, n): - """ - Access the n'th message in the album. - - Equivalent to ``event.messages[n]``. - """ - return self.messages[n] + Equivalent to ``event.messages[n]``. + """ + return self.messages[n] diff --git a/telethon/_events/base.py b/telethon/_events/base.py index 8f913ad7..303c5976 100644 --- a/telethon/_events/base.py +++ b/telethon/_events/base.py @@ -1,7 +1,4 @@ -from .raw import Raw - - -_HANDLERS_ATTRIBUTE = '__tl.handlers' +import abc class StopPropagation(Exception): @@ -31,101 +28,16 @@ class StopPropagation(Exception): pass -def register(event=None): - """ - Decorator method to *register* event handlers. This is the client-less - `add_event_handler() - ` variant. +class EventBuilder(abc.ABC): + @classmethod + @abc.abstractmethod + def _build(cls, update, others, self_id, entities, client): + """ + Builds an event for the given update if possible, or returns None. - Note that this method only registers callbacks as handlers, - and does not attach them to any client. This is useful for - external modules that don't have access to the client, but - still want to define themselves as a handler. Example: + `others` are the rest of updates that came in the same container + as the current `update`. - >>> from telethon import events - >>> @events.register(events.NewMessage) - ... async def handler(event): - ... ... - ... - >>> # (somewhere else) - ... - >>> from telethon import TelegramClient - >>> client = TelegramClient(...) - >>> client.add_event_handler(handler) - - Remember that you can use this as a non-decorator - through ``register(event)(callback)``. - - Args: - event (`_EventBuilder` | `type`): - The event builder class or instance to be used, - for instance ``events.NewMessage``. - """ - if isinstance(event, type): - event = event() - elif not event: - event = Raw() - - def decorator(callback): - handlers = getattr(callback, _HANDLERS_ATTRIBUTE, []) - handlers.append(event) - setattr(callback, _HANDLERS_ATTRIBUTE, handlers) - return callback - - return decorator - - -def unregister(callback, event=None): - """ - Inverse operation of `register` (though not a decorator). Client-less - `remove_event_handler - ` - variant. **Note that this won't remove handlers from the client**, - because it simply can't, so you would generally use this before - adding the handlers to the client. - - This method is here for symmetry. You will rarely need to - unregister events, since you can simply just not add them - to any client. - - If no event is given, all events for this callback are removed. - Returns how many callbacks were removed. - """ - found = 0 - if event and not isinstance(event, type): - event = type(event) - - handlers = getattr(callback, _HANDLERS_ATTRIBUTE, []) - handlers.append((event, callback)) - i = len(handlers) - while i: - i -= 1 - ev = handlers[i] - if not event or isinstance(ev, event): - del handlers[i] - found += 1 - - return found - - -def is_handler(callback): - """ - Returns `True` if the given callback is an - event handler (i.e. you used `register` on it). - """ - return hasattr(callback, _HANDLERS_ATTRIBUTE) - - -def list(callback): - """ - Returns a list containing the registered event - builders inside the specified callback handler. - """ - return getattr(callback, _HANDLERS_ATTRIBUTE, [])[:] - - -def _get_handlers(callback): - """ - Like ``list`` but returns `None` if the callback was never registered. - """ - return getattr(callback, _HANDLERS_ATTRIBUTE, None) + `self_id` should be the current user's ID, since it is required + for some events which lack this information but still need it. + """ diff --git a/telethon/_events/callbackquery.py b/telethon/_events/callbackquery.py index 0e3e2d67..8d298349 100644 --- a/telethon/_events/callbackquery.py +++ b/telethon/_events/callbackquery.py @@ -3,7 +3,7 @@ import struct import asyncio import functools -from .common import EventBuilder, EventCommon, name_inner_event +from .base import EventBuilder from .._misc import utils from .. import _tl from ..types import _custom @@ -23,8 +23,7 @@ def auto_answer(func): return wrapped -@name_inner_event -class CallbackQuery(EventBuilder): +class CallbackQuery(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.SenderGetter): """ Occurs whenever you sign in as a bot and a user clicks one of the inline buttons on your messages. @@ -34,18 +33,17 @@ class CallbackQuery(EventBuilder): message. The `chats` parameter also supports checking against the `chat_instance` which should be used for inline callbacks. - Args: - data (`bytes`, `str`, `callable`, optional): - If set, the inline button payload data must match this data. - A UTF-8 string can also be given, a regex or a callable. For - instance, to check against ``'data_1'`` and ``'data_2'`` you - can use ``re.compile(b'data_')``. + Members: + query (:tl:`UpdateBotCallbackQuery`): + The original :tl:`UpdateBotCallbackQuery`. - pattern (`bytes`, `str`, `callable`, `Pattern`, optional): - If set, only buttons with payload matching this pattern will be handled. - You can specify a regex-like string which will be matched - against the payload data, a callable function that returns `True` - if a the payload data is acceptable, or a compiled regex pattern. + data_match (`obj`, optional): + The object returned by the ``data=`` parameter + when creating the event builder, if any. Similar + to ``pattern_match`` for the new message event. + + pattern_match (`obj`, optional): + Alias for ``data_match``. Example .. code-block:: python @@ -71,39 +69,17 @@ class CallbackQuery(EventBuilder): Button.inline('Nope', b'no') ]) """ - def __init__( - self, chats=None, *, blacklist_chats=False, func=None, data=None, pattern=None): - super().__init__(chats, blacklist_chats=blacklist_chats, func=func) - - if data and pattern: - raise ValueError("Only pass either data or pattern not both.") - - if isinstance(data, str): - data = data.encode('utf-8') - if isinstance(pattern, str): - pattern = pattern.encode('utf-8') - - match = data if data else pattern - - if isinstance(match, bytes): - self.match = data if data else re.compile(pattern).match - elif not match or callable(match): - self.match = match - elif hasattr(match, 'match') and callable(match.match): - if not isinstance(getattr(match, 'pattern', b''), bytes): - match = re.compile(match.pattern.encode('utf-8'), - match.flags & (~re.UNICODE)) - - self.match = match.match - else: - raise TypeError('Invalid data or pattern type given') - - self._no_check = all(x is None for x in ( - self.chats, self.func, self.match, - )) + def __init__(self, query, peer, msg_id): + _custom.chatgetter.ChatGetter.__init__(self, peer) + _custom.sendergetter.SenderGetter.__init__(self, query.user_id) + self.query = query + self.data_match = None + self.pattern_match = None + self._message = None + self._answered = False @classmethod - def build(cls, update, others=None, self_id=None, *todo, **todo2): + def _build(cls, update, others=None, self_id=None, *todo, **todo2): if isinstance(update, _tl.UpdateBotCallbackQuery): return cls.Event(update, update.peer, update.msg_id) elif isinstance(update, _tl.UpdateInlineBotCallbackQuery): @@ -113,242 +89,191 @@ class CallbackQuery(EventBuilder): peer = _tl.PeerChannel(-pid) if pid < 0 else _tl.PeerUser(pid) return cls.Event(update, peer, mid) - def filter(self, event): - # We can't call super().filter(...) because it ignores chat_instance - if self._no_check: - return event + def _set_client(self, client): + super()._set_client(client) + self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities) - if self.chats is not None: - inside = event.query.chat_instance in self.chats - if event.chat_id: - inside |= event.chat_id in self.chats - - if inside == self.blacklist_chats: - return - - if self.match: - if callable(self.match): - event.data_match = event.pattern_match = self.match(event.query.data) - if not event.data_match: - return - elif event.query.data != self.match: - return - - if self.func: - # Return the result of func directly as it may need to be awaited - return self.func(event) - return True - - class Event(EventCommon, _custom.sendergetter.SenderGetter): + @property + def id(self): """ - Represents the event of a new callback query. - - Members: - query (:tl:`UpdateBotCallbackQuery`): - The original :tl:`UpdateBotCallbackQuery`. - - data_match (`obj`, optional): - The object returned by the ``data=`` parameter - when creating the event builder, if any. Similar - to ``pattern_match`` for the new message event. - - pattern_match (`obj`, optional): - Alias for ``data_match``. + Returns the query ID. The user clicking the inline + button is the one who generated this random ID. """ - def __init__(self, query, peer, msg_id): - super().__init__(peer, msg_id=msg_id) - _custom.sendergetter.SenderGetter.__init__(self, query.user_id) - self.query = query - self.data_match = None - self.pattern_match = None - self._message = None - self._answered = False + return self.query.query_id - def _set_client(self, client): - super()._set_client(client) - self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities) + @property + def message_id(self): + """ + Returns the message ID to which the clicked inline button belongs. + """ + return self._message_id - @property - def id(self): - """ - Returns the query ID. The user clicking the inline - button is the one who generated this random ID. - """ - return self.query.query_id + @property + def data(self): + """ + Returns the data payload from the original inline button. + """ + return self.query.data - @property - def message_id(self): - """ - Returns the message ID to which the clicked inline button belongs. - """ - return self._message_id - - @property - def data(self): - """ - Returns the data payload from the original inline button. - """ - return self.query.data - - @property - def chat_instance(self): - """ - Unique identifier for the chat where the callback occurred. - Useful for high scores in games. - """ - return self.query.chat_instance - - async def get_message(self): - """ - Returns the message to which the clicked inline button belongs. - """ - if self._message is not None: - return self._message - - try: - chat = await self.get_input_chat() if self.is_channel else None - self._message = await self._client.get_messages( - chat, ids=self._message_id) - except ValueError: - return + @property + def chat_instance(self): + """ + Unique identifier for the chat where the callback occurred. + Useful for high scores in games. + """ + return self.query.chat_instance + async def get_message(self): + """ + Returns the message to which the clicked inline button belongs. + """ + if self._message is not None: return self._message - async def _refetch_sender(self): - self._sender = self._entities.get(self.sender_id) - if not self._sender: - return + try: + chat = await self.get_input_chat() if self.is_channel else None + self._message = await self._client.get_messages( + chat, ids=self._message_id) + except ValueError: + return - self._input_sender = utils.get_input_peer(self._chat) - if not getattr(self._input_sender, 'access_hash', True): - # getattr with True to handle the InputPeerSelf() case - m = await self.get_message() - if m: - self._sender = m._sender - self._input_sender = m._input_sender + return self._message - async def answer( - self, message=None, cache_time=0, *, url=None, alert=False): - """ - Answers the callback query (and stops the loading circle). + async def _refetch_sender(self): + self._sender = self._entities.get(self.sender_id) + if not self._sender: + return - Args: - message (`str`, optional): - The toast message to show feedback to the user. + self._input_sender = utils.get_input_peer(self._chat) + if not getattr(self._input_sender, 'access_hash', True): + # getattr with True to handle the InputPeerSelf() case + m = await self.get_message() + if m: + self._sender = m._sender + self._input_sender = m._input_sender - cache_time (`int`, optional): - For how long this result should be cached on - the user's client. Defaults to 0 for no cache. + async def answer( + self, message=None, cache_time=0, *, url=None, alert=False): + """ + Answers the callback query (and stops the loading circle). - url (`str`, optional): - The URL to be opened in the user's client. Note that - the only valid URLs are those of games your bot has, - or alternatively a 't.me/your_bot?start=xyz' parameter. + Args: + message (`str`, optional): + The toast message to show feedback to the user. - alert (`bool`, optional): - Whether an alert (a pop-up dialog) should be used - instead of showing a toast. Defaults to `False`. - """ - if self._answered: - return + cache_time (`int`, optional): + For how long this result should be cached on + the user's client. Defaults to 0 for no cache. - res = await self._client(_tl.fn.messages.SetBotCallbackAnswer( - query_id=self.query.query_id, - cache_time=cache_time, - alert=alert, - message=message, - url=url, - )) - self._answered = True - return res + url (`str`, optional): + The URL to be opened in the user's client. Note that + the only valid URLs are those of games your bot has, + or alternatively a 't.me/your_bot?start=xyz' parameter. - @property - def via_inline(self): - """ - Whether this callback was generated from an inline button sent - via an inline query or not. If the bot sent the message itself - with buttons, and one of those is clicked, this will be `False`. - If a user sent the message coming from an inline query to the - bot, and one of those is clicked, this will be `True`. + alert (`bool`, optional): + Whether an alert (a pop-up dialog) should be used + instead of showing a toast. Defaults to `False`. + """ + if self._answered: + return - If it's `True`, it's likely that the bot is **not** in the - chat, so methods like `respond` or `delete` won't work (but - `edit` will always work). - """ - return isinstance(self.query, _tl.UpdateInlineBotCallbackQuery) + res = await self._client(_tl.fn.messages.SetBotCallbackAnswer( + query_id=self.query.query_id, + cache_time=cache_time, + alert=alert, + message=message, + url=url, + )) + self._answered = True + return res - @auto_answer - async def respond(self, *args, **kwargs): - """ - Responds to the message (not as a reply). Shorthand for - `telethon.client.messages.MessageMethods.send_message` with - ``entity`` already set. + @property + def via_inline(self): + """ + Whether this callback was generated from an inline button sent + via an inline query or not. If the bot sent the message itself + with buttons, and one of those is clicked, this will be `False`. + If a user sent the message coming from an inline query to the + bot, and one of those is clicked, this will be `True`. - This method will also `answer` the callback if necessary. + If it's `True`, it's likely that the bot is **not** in the + chat, so methods like `respond` or `delete` won't work (but + `edit` will always work). + """ + return isinstance(self.query, _tl.UpdateInlineBotCallbackQuery) - This method will likely fail if `via_inline` is `True`. - """ - return await self._client.send_message( - await self.get_input_chat(), *args, **kwargs) + @auto_answer + async def respond(self, *args, **kwargs): + """ + Responds to the message (not as a reply). Shorthand for + `telethon.client.messages.MessageMethods.send_message` with + ``entity`` already set. - @auto_answer - async def reply(self, *args, **kwargs): - """ - Replies to the message (as a reply). Shorthand for - `telethon.client.messages.MessageMethods.send_message` with - both ``entity`` and ``reply_to`` already set. + This method will also `answer` the callback if necessary. - This method will also `answer` the callback if necessary. + This method will likely fail if `via_inline` is `True`. + """ + return await self._client.send_message( + await self.get_input_chat(), *args, **kwargs) - This method will likely fail if `via_inline` is `True`. - """ - kwargs['reply_to'] = self.query.msg_id - return await self._client.send_message( - await self.get_input_chat(), *args, **kwargs) + @auto_answer + async def reply(self, *args, **kwargs): + """ + Replies to the message (as a reply). Shorthand for + `telethon.client.messages.MessageMethods.send_message` with + both ``entity`` and ``reply_to`` already set. - @auto_answer - async def edit(self, *args, **kwargs): - """ - Edits the message. Shorthand for - `telethon.client.messages.MessageMethods.edit_message` with - the ``entity`` set to the correct :tl:`InputBotInlineMessageID`. + This method will also `answer` the callback if necessary. - Returns `True` if the edit was successful. + This method will likely fail if `via_inline` is `True`. + """ + kwargs['reply_to'] = self.query.msg_id + return await self._client.send_message( + await self.get_input_chat(), *args, **kwargs) - This method will also `answer` the callback if necessary. + @auto_answer + async def edit(self, *args, **kwargs): + """ + Edits the message. Shorthand for + `telethon.client.messages.MessageMethods.edit_message` with + the ``entity`` set to the correct :tl:`InputBotInlineMessageID`. - .. note:: + Returns `True` if the edit was successful. - This method won't respect the previous message unlike - `Message.edit `, - since the message object is normally not present. - """ - if isinstance(self.query.msg_id, _tl.InputBotInlineMessageID): - return await self._client.edit_message( - None, self.query.msg_id, *args, **kwargs - ) - else: - return await self._client.edit_message( - await self.get_input_chat(), self.query.msg_id, - *args, **kwargs - ) + This method will also `answer` the callback if necessary. - @auto_answer - async def delete(self, *args, **kwargs): - """ - Deletes the message. Shorthand for - `telethon.client.messages.MessageMethods.delete_messages` with - ``entity`` and ``message_ids`` already set. + .. note:: - If you need to delete more than one message at once, don't use - this `delete` method. Use a - `telethon.client.telegramclient.TelegramClient` instance directly. - - This method will also `answer` the callback if necessary. - - This method will likely fail if `via_inline` is `True`. - """ - return await self._client.delete_messages( - await self.get_input_chat(), [self.query.msg_id], + This method won't respect the previous message unlike + `Message.edit `, + since the message object is normally not present. + """ + if isinstance(self.query.msg_id, _tl.InputBotInlineMessageID): + return await self._client.edit_message( + None, self.query.msg_id, *args, **kwargs + ) + else: + return await self._client.edit_message( + await self.get_input_chat(), self.query.msg_id, *args, **kwargs ) + + @auto_answer + async def delete(self, *args, **kwargs): + """ + Deletes the message. Shorthand for + `telethon.client.messages.MessageMethods.delete_messages` with + ``entity`` and ``message_ids`` already set. + + If you need to delete more than one message at once, don't use + this `delete` method. Use a + `telethon.client.telegramclient.TelegramClient` instance directly. + + This method will also `answer` the callback if necessary. + + This method will likely fail if `via_inline` is `True`. + """ + return await self._client.delete_messages( + await self.get_input_chat(), [self.query.msg_id], + *args, **kwargs + ) diff --git a/telethon/_events/chataction.py b/telethon/_events/chataction.py index 0bf83aa1..1e9b7271 100644 --- a/telethon/_events/chataction.py +++ b/telethon/_events/chataction.py @@ -1,10 +1,9 @@ -from .common import EventBuilder, EventCommon, name_inner_event +from .base import EventBuilder from .._misc import utils from .. import _tl from ..types import _custom -@name_inner_event class ChatAction(EventBuilder): """ Occurs on certain chat actions: @@ -20,6 +19,47 @@ class ChatAction(EventBuilder): Note that "chat" refers to "small group, megagroup and broadcast channel", whereas "group" refers to "small group and megagroup" only. + Members: + action_message (`MessageAction `_): + The message invoked by this Chat Action. + + new_pin (`bool`): + `True` if there is a new pin. + + new_photo (`bool`): + `True` if there's a new chat photo (or it was removed). + + photo (:tl:`Photo`, optional): + The new photo (or `None` if it was removed). + + user_added (`bool`): + `True` if the user was added by some other. + + user_joined (`bool`): + `True` if the user joined on their own. + + user_left (`bool`): + `True` if the user left on their own. + + user_kicked (`bool`): + `True` if the user was kicked by some other. + + user_approved (`bool`): + `True` if the user's join request was approved. + along with `user_joined` will be also True. + + created (`bool`, optional): + `True` if this chat was just created. + + new_title (`str`, optional): + The new title string for the chat, if applicable. + + new_score (`str`, optional): + The new score string for the game, if applicable. + + unpin (`bool`): + `True` if the existing pin gets unpinned. + Example .. code-block:: python @@ -32,8 +72,64 @@ class ChatAction(EventBuilder): await event.reply('Welcome to the group!') """ + def __init__(self, where, new_photo=None, + added_by=None, kicked_by=None, created=None, from_approval=None, + users=None, new_title=None, pin_ids=None, pin=None, new_score=None): + if isinstance(where, _tl.MessageService): + self.action_message = where + where = where.peer_id + else: + self.action_message = None + + # TODO needs some testing (can there be more than one id, and do they follow pin order?) + # same in get_pinned_message + super().__init__(chat_peer=where, msg_id=pin_ids[0] if pin_ids else None) + + self.new_pin = pin_ids is not None + self._pin_ids = pin_ids + self._pinned_messages = None + + self.new_photo = new_photo is not None + self.photo = \ + new_photo if isinstance(new_photo, _tl.Photo) else None + + self._added_by = None + self._kicked_by = None + self.user_added = self.user_joined = self.user_left = \ + self.user_kicked = self.unpin = False + + if added_by is True or from_approval is True: + self.user_joined = True + elif added_by: + self.user_added = True + self._added_by = added_by + self.user_approved = from_approval + + # If `from_id` was not present (it's `True`) or the affected + # user was "kicked by itself", then it left. Else it was kicked. + if kicked_by is True or (users is not None and kicked_by == users): + self.user_left = True + elif kicked_by: + self.user_kicked = True + self._kicked_by = kicked_by + + self.created = bool(created) + + if isinstance(users, list): + self._user_ids = [utils.get_peer_id(u) for u in users] + elif users: + self._user_ids = [utils.get_peer_id(users)] + else: + self._user_ids = [] + + self._users = None + self._input_users = None + self.new_title = new_title + self.new_score = new_score + self.unpin = not pin + @classmethod - def build(cls, update, others=None, self_id=None, *todo, **todo2): + def _build(cls, update, others=None, self_id=None, *todo, **todo2): # Rely on specific pin updates for unpins, but otherwise ignore them # for new pins (we'd rather handle the new service message with pin, # so that we can act on that message'). @@ -114,332 +210,230 @@ class ChatAction(EventBuilder): return cls.Event(msg, new_score=action.score) - class Event(EventCommon): + async def respond(self, *args, **kwargs): """ - Represents the event of a new chat action. - - Members: - action_message (`MessageAction `_): - The message invoked by this Chat Action. - - new_pin (`bool`): - `True` if there is a new pin. - - new_photo (`bool`): - `True` if there's a new chat photo (or it was removed). - - photo (:tl:`Photo`, optional): - The new photo (or `None` if it was removed). - - user_added (`bool`): - `True` if the user was added by some other. - - user_joined (`bool`): - `True` if the user joined on their own. - - user_left (`bool`): - `True` if the user left on their own. - - user_kicked (`bool`): - `True` if the user was kicked by some other. - - user_approved (`bool`): - `True` if the user's join request was approved. - along with `user_joined` will be also True. - - created (`bool`, optional): - `True` if this chat was just created. - - new_title (`str`, optional): - The new title string for the chat, if applicable. - - new_score (`str`, optional): - The new score string for the game, if applicable. - - unpin (`bool`): - `True` if the existing pin gets unpinned. + Responds to the chat action message (not as a reply). Shorthand for + `telethon.client.messages.MessageMethods.send_message` with + ``entity`` already set. """ + return await self._client.send_message( + await self.get_input_chat(), *args, **kwargs) - def __init__(self, where, new_photo=None, - added_by=None, kicked_by=None, created=None, from_approval=None, - users=None, new_title=None, pin_ids=None, pin=None, new_score=None): - if isinstance(where, _tl.MessageService): - self.action_message = where - where = where.peer_id - else: - self.action_message = None + async def reply(self, *args, **kwargs): + """ + Replies to the chat action message (as a reply). Shorthand for + `telethon.client.messages.MessageMethods.send_message` with + both ``entity`` and ``reply_to`` already set. - # TODO needs some testing (can there be more than one id, and do they follow pin order?) - # same in get_pinned_message - super().__init__(chat_peer=where, msg_id=pin_ids[0] if pin_ids else None) + Has the same effect as `respond` if there is no message. + """ + if not self.action_message: + return await self.respond(*args, **kwargs) - self.new_pin = pin_ids is not None - self._pin_ids = pin_ids - self._pinned_messages = None + kwargs['reply_to'] = self.action_message.id + return await self._client.send_message( + await self.get_input_chat(), *args, **kwargs) - self.new_photo = new_photo is not None - self.photo = \ - new_photo if isinstance(new_photo, _tl.Photo) else None + async def delete(self, *args, **kwargs): + """ + Deletes the chat action message. You're responsible for checking + whether you have the permission to do so, or to except the error + otherwise. Shorthand for + `telethon.client.messages.MessageMethods.delete_messages` with + ``entity`` and ``message_ids`` already set. - self._added_by = None - self._kicked_by = None - self.user_added = self.user_joined = self.user_left = \ - self.user_kicked = self.unpin = False + Does nothing if no message action triggered this event. + """ + if not self.action_message: + return - if added_by is True or from_approval is True: - self.user_joined = True - elif added_by: - self.user_added = True - self._added_by = added_by - self.user_approved = from_approval + return await self._client.delete_messages( + await self.get_input_chat(), [self.action_message], + *args, **kwargs + ) - # If `from_id` was not present (it's `True`) or the affected - # user was "kicked by itself", then it left. Else it was kicked. - if kicked_by is True or (users is not None and kicked_by == users): - self.user_left = True - elif kicked_by: - self.user_kicked = True - self._kicked_by = kicked_by + async def get_pinned_message(self): + """ + If ``new_pin`` is `True`, this returns the `Message + ` object that was pinned. + """ + if self._pinned_messages is None: + await self.get_pinned_messages() - self.created = bool(created) + if self._pinned_messages: + return self._pinned_messages[0] - if isinstance(users, list): - self._user_ids = [utils.get_peer_id(u) for u in users] - elif users: - self._user_ids = [utils.get_peer_id(users)] - else: - self._user_ids = [] + async def get_pinned_messages(self): + """ + If ``new_pin`` is `True`, this returns a `list` of `Message + ` objects that were pinned. + """ + if not self._pin_ids: + return self._pin_ids # either None or empty list - self._users = None - self._input_users = None - self.new_title = new_title - self.new_score = new_score - self.unpin = not pin + chat = await self.get_input_chat() + if chat: + self._pinned_messages = await self._client.get_messages( + self._input_chat, ids=self._pin_ids) - async def respond(self, *args, **kwargs): - """ - Responds to the chat action message (not as a reply). Shorthand for - `telethon.client.messages.MessageMethods.send_message` with - ``entity`` already set. - """ - return await self._client.send_message( - await self.get_input_chat(), *args, **kwargs) + return self._pinned_messages - async def reply(self, *args, **kwargs): - """ - Replies to the chat action message (as a reply). Shorthand for - `telethon.client.messages.MessageMethods.send_message` with - both ``entity`` and ``reply_to`` already set. + @property + def added_by(self): + """ + The user who added ``users``, if applicable (`None` otherwise). + """ + if self._added_by and not isinstance(self._added_by, _tl.User): + aby = self._entities.get(utils.get_peer_id(self._added_by)) + if aby: + self._added_by = aby - Has the same effect as `respond` if there is no message. - """ - if not self.action_message: - return await self.respond(*args, **kwargs) + return self._added_by - kwargs['reply_to'] = self.action_message.id - return await self._client.send_message( - await self.get_input_chat(), *args, **kwargs) + async def get_added_by(self): + """ + Returns `added_by` but will make an API call if necessary. + """ + if not self.added_by and self._added_by: + self._added_by = await self._client.get_entity(self._added_by) - async def delete(self, *args, **kwargs): - """ - Deletes the chat action message. You're responsible for checking - whether you have the permission to do so, or to except the error - otherwise. Shorthand for - `telethon.client.messages.MessageMethods.delete_messages` with - ``entity`` and ``message_ids`` already set. + return self._added_by - Does nothing if no message action triggered this event. - """ - if not self.action_message: - return + @property + def kicked_by(self): + """ + The user who kicked ``users``, if applicable (`None` otherwise). + """ + if self._kicked_by and not isinstance(self._kicked_by, _tl.User): + kby = self._entities.get(utils.get_peer_id(self._kicked_by)) + if kby: + self._kicked_by = kby - return await self._client.delete_messages( - await self.get_input_chat(), [self.action_message], - *args, **kwargs - ) + return self._kicked_by - async def get_pinned_message(self): - """ - If ``new_pin`` is `True`, this returns the `Message - ` object that was pinned. - """ - if self._pinned_messages is None: - await self.get_pinned_messages() + async def get_kicked_by(self): + """ + Returns `kicked_by` but will make an API call if necessary. + """ + if not self.kicked_by and self._kicked_by: + self._kicked_by = await self._client.get_entity(self._kicked_by) - if self._pinned_messages: - return self._pinned_messages[0] + return self._kicked_by - async def get_pinned_messages(self): - """ - If ``new_pin`` is `True`, this returns a `list` of `Message - ` objects that were pinned. - """ - if not self._pin_ids: - return self._pin_ids # either None or empty list + @property + def user(self): + """ + The first user that takes part in this action. For example, who joined. - chat = await self.get_input_chat() - if chat: - self._pinned_messages = await self._client.get_messages( - self._input_chat, ids=self._pin_ids) + Might be `None` if the information can't be retrieved or + there is no user taking part. + """ + if self.users: + return self._users[0] - return self._pinned_messages + async def get_user(self): + """ + Returns `user` but will make an API call if necessary. + """ + if self.users or await self.get_users(): + return self._users[0] - @property - def added_by(self): - """ - The user who added ``users``, if applicable (`None` otherwise). - """ - if self._added_by and not isinstance(self._added_by, _tl.User): - aby = self._entities.get(utils.get_peer_id(self._added_by)) - if aby: - self._added_by = aby + @property + def input_user(self): + """ + Input version of the ``self.user`` property. + """ + if self.input_users: + return self._input_users[0] - return self._added_by + async def get_input_user(self): + """ + Returns `input_user` but will make an API call if necessary. + """ + if self.input_users or await self.get_input_users(): + return self._input_users[0] - async def get_added_by(self): - """ - Returns `added_by` but will make an API call if necessary. - """ - if not self.added_by and self._added_by: - self._added_by = await self._client.get_entity(self._added_by) + @property + def user_id(self): + """ + Returns the marked signed ID of the first user, if any. + """ + if self._user_ids: + return self._user_ids[0] - return self._added_by + @property + def users(self): + """ + A list of users that take part in this action. For example, who joined. - @property - def kicked_by(self): - """ - The user who kicked ``users``, if applicable (`None` otherwise). - """ - if self._kicked_by and not isinstance(self._kicked_by, _tl.User): - kby = self._entities.get(utils.get_peer_id(self._kicked_by)) - if kby: - self._kicked_by = kby + Might be empty if the information can't be retrieved or there + are no users taking part. + """ + if not self._user_ids: + return [] - return self._kicked_by + if self._users is None: + self._users = [ + self._entities[user_id] + for user_id in self._user_ids + if user_id in self._entities + ] - async def get_kicked_by(self): - """ - Returns `kicked_by` but will make an API call if necessary. - """ - if not self.kicked_by and self._kicked_by: - self._kicked_by = await self._client.get_entity(self._kicked_by) + return self._users - return self._kicked_by + async def get_users(self): + """ + Returns `users` but will make an API call if necessary. + """ + if not self._user_ids: + return [] - @property - def user(self): - """ - The first user that takes part in this action. For example, who joined. + # Note: we access the property first so that it fills if needed + if (self.users is None or len(self._users) != len(self._user_ids)) and self.action_message: + await self.action_message._reload_message() + self._users = [ + u for u in self.action_message.action_entities + if isinstance(u, (_tl.User, _tl.UserEmpty))] - Might be `None` if the information can't be retrieved or - there is no user taking part. - """ - if self.users: - return self._users[0] + return self._users - async def get_user(self): - """ - Returns `user` but will make an API call if necessary. - """ - if self.users or await self.get_users(): - return self._users[0] + @property + def input_users(self): + """ + Input version of the ``self.users`` property. + """ + if self._input_users is None and self._user_ids: + self._input_users = [] + for user_id in self._user_ids: + # Try to get it from our entities + try: + self._input_users.append(utils.get_input_peer(self._entities[user_id])) + continue + except (KeyError, TypeError): + pass - @property - def input_user(self): - """ - Input version of the ``self.user`` property. - """ - if self.input_users: - return self._input_users[0] + return self._input_users or [] - async def get_input_user(self): - """ - Returns `input_user` but will make an API call if necessary. - """ - if self.input_users or await self.get_input_users(): - return self._input_users[0] + async def get_input_users(self): + """ + Returns `input_users` but will make an API call if necessary. + """ + if not self._user_ids: + return [] - @property - def user_id(self): - """ - Returns the marked signed ID of the first user, if any. - """ - if self._user_ids: - return self._user_ids[0] + # Note: we access the property first so that it fills if needed + if (self.input_users is None or len(self._input_users) != len(self._user_ids)) and self.action_message: + self._input_users = [ + utils.get_input_peer(u) + for u in self.action_message.action_entities + if isinstance(u, (_tl.User, _tl.UserEmpty))] - @property - def users(self): - """ - A list of users that take part in this action. For example, who joined. + return self._input_users or [] - Might be empty if the information can't be retrieved or there - are no users taking part. - """ - if not self._user_ids: - return [] - - if self._users is None: - self._users = [ - self._entities[user_id] - for user_id in self._user_ids - if user_id in self._entities - ] - - return self._users - - async def get_users(self): - """ - Returns `users` but will make an API call if necessary. - """ - if not self._user_ids: - return [] - - # Note: we access the property first so that it fills if needed - if (self.users is None or len(self._users) != len(self._user_ids)) and self.action_message: - await self.action_message._reload_message() - self._users = [ - u for u in self.action_message.action_entities - if isinstance(u, (_tl.User, _tl.UserEmpty))] - - return self._users - - @property - def input_users(self): - """ - Input version of the ``self.users`` property. - """ - if self._input_users is None and self._user_ids: - self._input_users = [] - for user_id in self._user_ids: - # Try to get it from our entities - try: - self._input_users.append(utils.get_input_peer(self._entities[user_id])) - continue - except (KeyError, TypeError): - pass - - return self._input_users or [] - - async def get_input_users(self): - """ - Returns `input_users` but will make an API call if necessary. - """ - if not self._user_ids: - return [] - - # Note: we access the property first so that it fills if needed - if (self.input_users is None or len(self._input_users) != len(self._user_ids)) and self.action_message: - self._input_users = [ - utils.get_input_peer(u) - for u in self.action_message.action_entities - if isinstance(u, (_tl.User, _tl.UserEmpty))] - - return self._input_users or [] - - @property - def user_ids(self): - """ - Returns the marked signed ID of the users, if any. - """ - if self._user_ids: - return self._user_ids[:] + @property + def user_ids(self): + """ + Returns the marked signed ID of the users, if any. + """ + if self._user_ids: + return self._user_ids[:] diff --git a/telethon/_events/common.py b/telethon/_events/common.py deleted file mode 100644 index c20ac64e..00000000 --- a/telethon/_events/common.py +++ /dev/null @@ -1,179 +0,0 @@ -import abc -import asyncio -import warnings - -from .. import _tl -from .._misc import utils, tlobject -from ..types._custom.chatgetter import ChatGetter - - -async def _into_id_set(client, chats): - """Helper util to turn the input chat or chats into a set of IDs.""" - if chats is None: - return None - - if not utils.is_list_like(chats): - chats = (chats,) - - result = set() - for chat in chats: - if isinstance(chat, int): - result.add(chat) - elif isinstance(chat, tlobject.TLObject) and chat.SUBCLASS_OF_ID == 0x2d45687: - # 0x2d45687 == crc32(b'Peer') - result.add(utils.get_peer_id(chat)) - else: - chat = await client.get_input_entity(chat) - if isinstance(chat, _tl.InputPeerSelf): - chat = _tl.PeerUser(self._session_state.user_id) - result.add(utils.get_peer_id(chat)) - - return result - - -class EventBuilder(abc.ABC): - """ - The common event builder, with builtin support to filter per chat. - - Args: - chats (`entity`, optional): - May be one or more entities (username/peer/etc.), preferably IDs. - By default, only matching chats will be handled. - - blacklist_chats (`bool`, optional): - Whether to treat the chats as a blacklist instead of - as a whitelist (default). This means that every chat - will be handled *except* those specified in ``chats`` - which will be ignored if ``blacklist_chats=True``. - - func (`callable`, optional): - A callable (async or not) function that should accept the event as input - parameter, and return a value indicating whether the event - should be dispatched or not (any truthy value will do, it - does not need to be a `bool`). It works like a custom filter: - - .. code-block:: python - - @client.on(events.NewMessage(func=lambda e: e.is_private)) - async def handler(event): - pass # code here - """ - def __init__(self, chats=None, *, blacklist_chats=False, func=None): - self.chats = chats - self.blacklist_chats = bool(blacklist_chats) - self.resolved = False - self.func = func - self._resolve_lock = None - - @classmethod - @abc.abstractmethod - def build(cls, update, others, self_id, entities, client): - """ - Builds an event for the given update if possible, or returns None. - - `others` are the rest of updates that came in the same container - as the current `update`. - - `self_id` should be the current user's ID, since it is required - for some events which lack this information but still need it. - """ - # TODO So many parameters specific to only some update types seems dirty - - async def resolve(self, client): - """Helper method to allow event builders to be resolved before usage""" - if self.resolved: - return - - if not self._resolve_lock: - self._resolve_lock = asyncio.Lock() - - async with self._resolve_lock: - if not self.resolved: - await self._resolve(client) - self.resolved = True - - async def _resolve(self, client): - self.chats = await _into_id_set(client, self.chats) - - def filter(self, event): - """ - Returns a truthy value if the event passed the filter and should be - used, or falsy otherwise. The return value may need to be awaited. - - The events must have been resolved before this can be called. - """ - if not self.resolved: - return - - if self.chats is not None: - # Note: the `event.chat_id` property checks if it's `None` for us - inside = event.chat_id in self.chats - if inside == self.blacklist_chats: - # If this chat matches but it's a blacklist ignore. - # If it doesn't match but it's a whitelist ignore. - return - - if not self.func: - return True - - # Return the result of func directly as it may need to be awaited - return self.func(event) - - -class EventCommon(ChatGetter, abc.ABC): - """ - Intermediate class with common things to all events. - - Remember that this class implements `ChatGetter - ` which - means you have access to all chat properties and methods. - - In addition, you can access the `original_update` - field which contains the original :tl:`Update`. - """ - _event_name = 'Event' - - def __init__(self, chat_peer=None, msg_id=None, broadcast=None): - super().__init__(chat_peer, broadcast=broadcast) - self._entities = {} - self._client = None - self._message_id = msg_id - self.original_update = None - - def _set_client(self, client): - """ - Setter so subclasses can act accordingly when the client is set. - """ - # TODO Nuke - self._client = client - if self._chat_peer: - self._chat, self._input_chat = utils._get_entity_pair(self.chat_id, self._entities) - else: - self._chat = self._input_chat = None - - @property - def client(self): - """ - The `telethon.TelegramClient` that created this event. - """ - return self._client - - def __str__(self): - return _tl.TLObject.pretty_format(self.to_dict()) - - def stringify(self): - return _tl.TLObject.pretty_format(self.to_dict(), indent=0) - - def to_dict(self): - d = {k: v for k, v in self.__dict__.items() if k[0] != '_'} - d['_'] = self._event_name - return d - - -def name_inner_event(cls): - """Decorator to rename cls.Event 'Event' as 'cls.Event'""" - if hasattr(cls, 'Event'): - cls.Event._event_name = '{}.Event'.format(cls.__name__) - else: - warnings.warn('Class {} does not have a inner Event'.format(cls)) - return cls diff --git a/telethon/_events/inlinequery.py b/telethon/_events/inlinequery.py index 59ad6baa..c6c3bba6 100644 --- a/telethon/_events/inlinequery.py +++ b/telethon/_events/inlinequery.py @@ -3,34 +3,27 @@ import re import asyncio -from .common import EventBuilder, EventCommon, name_inner_event +from .base import EventBuilder from .._misc import utils from .. import _tl from ..types import _custom -@name_inner_event -class InlineQuery(EventBuilder): +class InlineQuery(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.SenderGetter): """ Occurs whenever you sign in as a bot and a user sends an inline query such as ``@bot query``. - Args: - users (`entity`, optional): - May be one or more entities (username/peer/etc.), preferably IDs. - By default, only inline queries from these users will be handled. + Members: + query (:tl:`UpdateBotInlineQuery`): + The original :tl:`UpdateBotInlineQuery`. - blacklist_users (`bool`, optional): - Whether to treat the users as a blacklist instead of - as a whitelist (default). This means that every chat - will be handled *except* those specified in ``users`` - which will be ignored if ``blacklist_users=True``. + Make sure to access the `text` property of the query if + you want the text rather than the actual query object. - pattern (`str`, `callable`, `Pattern`, optional): - If set, only queries matching this pattern will be handled. - You can specify a regex-like string which will be matched - against the message, a callable function that returns `True` - if a message is acceptable, or a compiled regex pattern. + pattern_match (`obj`, optional): + The resulting object from calling the passed ``pattern`` + function, which is ``re.compile(...).match`` by default. Example .. code-block:: python @@ -47,200 +40,163 @@ class InlineQuery(EventBuilder): builder.article('lowercase', text=event.text.lower()), ]) """ - def __init__( - self, users=None, *, blacklist_users=False, func=None, pattern=None): - super().__init__(users, blacklist_chats=blacklist_users, func=func) - - if isinstance(pattern, str): - self.pattern = re.compile(pattern).match - elif not pattern or callable(pattern): - self.pattern = pattern - elif hasattr(pattern, 'match') and callable(pattern.match): - self.pattern = pattern.match - else: - raise TypeError('Invalid pattern type given') + def __init__(self, query): + _custom.chatgetter.ChatGetter.__init__(self, _tl.PeerUser(query.user_id)) + _custom.sendergetter.SenderGetter.__init__(self, query.user_id) + self.query = query + self.pattern_match = None + self._answered = False @classmethod - def build(cls, update, others=None, self_id=None, *todo, **todo2): + def _build(cls, update, others=None, self_id=None, *todo, **todo2): if isinstance(update, _tl.UpdateBotInlineQuery): return cls.Event(update) - def filter(self, event): - if self.pattern: - match = self.pattern(event.text) - if not match: - return - event.pattern_match = match + def _set_client(self, client): + super()._set_client(client) + self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities) - return super().filter(event) - - class Event(EventCommon, _custom.sendergetter.SenderGetter): + @property + def id(self): """ - Represents the event of a new callback query. - - Members: - query (:tl:`UpdateBotInlineQuery`): - The original :tl:`UpdateBotInlineQuery`. - - Make sure to access the `text` property of the query if - you want the text rather than the actual query object. - - pattern_match (`obj`, optional): - The resulting object from calling the passed ``pattern`` - function, which is ``re.compile(...).match`` by default. + Returns the unique identifier for the query ID. """ - def __init__(self, query): - super().__init__(chat_peer=_tl.PeerUser(query.user_id)) - _custom.sendergetter.SenderGetter.__init__(self, query.user_id) - self.query = query - self.pattern_match = None - self._answered = False + return self.query.query_id - def _set_client(self, client): - super()._set_client(client) - self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities) + @property + def text(self): + """ + Returns the text the user used to make the inline query. + """ + return self.query.query - @property - def id(self): - """ - Returns the unique identifier for the query ID. - """ - return self.query.query_id + @property + def offset(self): + """ + The string the user's client used as an offset for the query. + This will either be empty or equal to offsets passed to `answer`. + """ + return self.query.offset - @property - def text(self): - """ - Returns the text the user used to make the inline query. - """ - return self.query.query + @property + def geo(self): + """ + If the user location is requested when using inline mode + and the user's device is able to send it, this will return + the :tl:`GeoPoint` with the position of the user. + """ + return self.query.geo - @property - def offset(self): - """ - The string the user's client used as an offset for the query. - This will either be empty or equal to offsets passed to `answer`. - """ - return self.query.offset + @property + def builder(self): + """ + Returns a new `InlineBuilder + ` instance. - @property - def geo(self): - """ - If the user location is requested when using inline mode - and the user's device is able to send it, this will return - the :tl:`GeoPoint` with the position of the user. - """ - return self.query.geo + See the documentation for `builder` to know what kind of answers + can be given. + """ + return _custom.InlineBuilder(self._client) - @property - def builder(self): - """ - Returns a new `InlineBuilder - ` instance. - """ - return _custom.InlineBuilder(self._client) + async def answer( + self, results=None, cache_time=0, *, + gallery=False, next_offset=None, private=False, + switch_pm=None, switch_pm_param=''): + """ + Answers the inline query with the given results. - async def answer( - self, results=None, cache_time=0, *, - gallery=False, next_offset=None, private=False, - switch_pm=None, switch_pm_param=''): - """ - Answers the inline query with the given results. - - See the documentation for `builder` to know what kind of answers - can be given. - - Args: - results (`list`, optional): - A list of :tl:`InputBotInlineResult` to use. - You should use `builder` to create these: - - .. code-block:: python - - builder = inline.builder - r1 = builder.article('Be nice', text='Have a nice day') - r2 = builder.article('Be bad', text="I don't like you") - await inline.answer([r1, r2]) - - You can send up to 50 results as documented in - https://core.telegram.org/bots/api#answerinlinequery. - Sending more will raise ``ResultsTooMuchError``, - and you should consider using `next_offset` to - paginate them. - - cache_time (`int`, optional): - For how long this result should be cached on - the user's client. Defaults to 0 for no cache. - - gallery (`bool`, optional): - Whether the results should show as a gallery (grid) or not. - - next_offset (`str`, optional): - The offset the client will send when the user scrolls the - results and it repeats the request. - - private (`bool`, optional): - Whether the results should be cached by Telegram - (not private) or by the user's client (private). - - switch_pm (`str`, optional): - If set, this text will be shown in the results - to allow the user to switch to private messages. - - switch_pm_param (`str`, optional): - Optional parameter to start the bot with if - `switch_pm` was used. - - Example: + Args: + results (`list`, optional): + A list of :tl:`InputBotInlineResult` to use. + You should use `builder` to create these: .. code-block:: python - @bot.on(events.InlineQuery) - async def handler(event): - builder = event.builder + builder = inline.builder + r1 = builder.article('Be nice', text='Have a nice day') + r2 = builder.article('Be bad', text="I don't like you") + await inline.answer([r1, r2]) - rev_text = event.text[::-1] - await event.answer([ - builder.article('Reverse text', text=rev_text), - builder.photo('/path/to/photo.jpg') - ]) - """ - if self._answered: - return + You can send up to 50 results as documented in + https://core.telegram.org/bots/api#answerinlinequery. + Sending more will raise ``ResultsTooMuchError``, + and you should consider using `next_offset` to + paginate them. - if results: - futures = [self._as_future(x) for x in results] + cache_time (`int`, optional): + For how long this result should be cached on + the user's client. Defaults to 0 for no cache. - await asyncio.wait(futures) + gallery (`bool`, optional): + Whether the results should show as a gallery (grid) or not. - # All futures will be in the `done` *set* that `wait` returns. - # - # Precisely because it's a `set` and not a `list`, it - # will not preserve the order, but since all futures - # completed we can use our original, ordered `list`. - results = [x.result() for x in futures] - else: - results = [] + next_offset (`str`, optional): + The offset the client will send when the user scrolls the + results and it repeats the request. - if switch_pm: - switch_pm = _tl.InlineBotSwitchPM(switch_pm, switch_pm_param) + private (`bool`, optional): + Whether the results should be cached by Telegram + (not private) or by the user's client (private). - return await self._client( - _tl.fn.messages.SetInlineBotResults( - query_id=self.query.query_id, - results=results, - cache_time=cache_time, - gallery=gallery, - next_offset=next_offset, - private=private, - switch_pm=switch_pm - ) + switch_pm (`str`, optional): + If set, this text will be shown in the results + to allow the user to switch to private messages. + + switch_pm_param (`str`, optional): + Optional parameter to start the bot with if + `switch_pm` was used. + + Example: + + .. code-block:: python + + @bot.on(events.InlineQuery) + async def handler(event): + builder = event.builder + + rev_text = event.text[::-1] + await event.answer([ + builder.article('Reverse text', text=rev_text), + builder.photo('/path/to/photo.jpg') + ]) + """ + if self._answered: + return + + if results: + futures = [self._as_future(x) for x in results] + + await asyncio.wait(futures) + + # All futures will be in the `done` *set* that `wait` returns. + # + # Precisely because it's a `set` and not a `list`, it + # will not preserve the order, but since all futures + # completed we can use our original, ordered `list`. + results = [x.result() for x in futures] + else: + results = [] + + if switch_pm: + switch_pm = _tl.InlineBotSwitchPM(switch_pm, switch_pm_param) + + return await self._client( + _tl.fn.messages.SetInlineBotResults( + query_id=self.query.query_id, + results=results, + cache_time=cache_time, + gallery=gallery, + next_offset=next_offset, + private=private, + switch_pm=switch_pm ) + ) - @staticmethod - def _as_future(obj): - if inspect.isawaitable(obj): - return asyncio.ensure_future(obj) + @staticmethod + def _as_future(obj): + if inspect.isawaitable(obj): + return asyncio.ensure_future(obj) - f = asyncio.get_running_loop().create_future() - f.set_result(obj) - return f + f = asyncio.get_running_loop().create_future() + f.set_result(obj) + return f diff --git a/telethon/_events/messagedeleted.py b/telethon/_events/messagedeleted.py index 58f9ff5f..6e7603b3 100644 --- a/telethon/_events/messagedeleted.py +++ b/telethon/_events/messagedeleted.py @@ -1,9 +1,9 @@ -from .common import EventBuilder, EventCommon, name_inner_event +from .base import EventBuilder from .. import _tl +from ..types import _custom -@name_inner_event -class MessageDeleted(EventBuilder): +class MessageDeleted(EventBuilder, _custom.chatgetter.ChatGetter): """ Occurs whenever a message is deleted. Note that this event isn't 100% reliable, since Telegram doesn't always notify the clients that a message @@ -35,8 +35,13 @@ class MessageDeleted(EventBuilder): for msg_id in event.deleted_ids: print('Message', msg_id, 'was deleted in', event.chat_id) """ + def __init__(self, deleted_ids, peer): + _custom.chatgetter.ChatGetter.__init__(self, chat_peer=peer) + self.deleted_id = None if not deleted_ids else deleted_ids[0] + self.deleted_ids = deleted_ids + @classmethod - def build(cls, update, others=None, self_id=None, *todo, **todo2): + def _build(cls, update, others=None, self_id=None, *todo, **todo2): if isinstance(update, _tl.UpdateDeleteMessages): return cls.Event( deleted_ids=update.messages, @@ -47,11 +52,3 @@ class MessageDeleted(EventBuilder): deleted_ids=update.messages, peer=_tl.PeerChannel(update.channel_id) ) - - class Event(EventCommon): - def __init__(self, deleted_ids, peer): - super().__init__( - chat_peer=peer, msg_id=(deleted_ids or [0])[0] - ) - self.deleted_id = None if not deleted_ids else deleted_ids[0] - self.deleted_ids = deleted_ids diff --git a/telethon/_events/messageedited.py b/telethon/_events/messageedited.py index 3f430a68..38c512ec 100644 --- a/telethon/_events/messageedited.py +++ b/telethon/_events/messageedited.py @@ -1,10 +1,8 @@ -from .common import name_inner_event -from .newmessage import NewMessage +from .base import EventBuilder from .. import _tl -@name_inner_event -class MessageEdited(NewMessage): +class MessageEdited(EventBuilder): """ Occurs whenever a message is edited. Just like `NewMessage `, you should treat @@ -43,10 +41,7 @@ class MessageEdited(NewMessage): print('Message', event.id, 'changed at', event.date) """ @classmethod - def build(cls, update, others=None, self_id=None, *todo, **todo2): + def _build(cls, update, others=None, self_id=None, *todo, **todo2): if isinstance(update, (_tl.UpdateEditMessage, _tl.UpdateEditChannelMessage)): return cls.Event(update.message) - - class Event(NewMessage.Event): - pass # Required if we want a different name for it diff --git a/telethon/_events/messageread.py b/telethon/_events/messageread.py index 0cd50de0..08f145dc 100644 --- a/telethon/_events/messageread.py +++ b/telethon/_events/messageread.py @@ -1,18 +1,24 @@ -from .common import EventBuilder, EventCommon, name_inner_event +from .base import EventBuilder from .._misc import utils from .. import _tl -@name_inner_event class MessageRead(EventBuilder): """ Occurs whenever one or more messages are read in a chat. - Args: - inbox (`bool`, optional): - If this argument is `True`, then when you read someone else's - messages the event will be fired. By default (`False`) only - when messages you sent are read by someone else will fire it. + Members: + max_id (`int`): + Up to which message ID has been read. Every message + with an ID equal or lower to it have been read. + + outbox (`bool`): + `True` if someone else has read your messages. + + contents (`bool`): + `True` if what was read were the contents of a message. + This will be the case when e.g. you play a voice note. + It may only be set on ``inbox`` events. Example .. code-block:: python @@ -29,13 +35,17 @@ class MessageRead(EventBuilder): # Log when you read message in a chat (from your "inbox") print('You have read messages until', event.max_id) """ - def __init__( - self, chats=None, *, blacklist_chats=False, func=None, inbox=False): - super().__init__(chats, blacklist_chats=blacklist_chats, func=func) - self.inbox = inbox + def __init__(self, peer=None, max_id=None, out=False, contents=False, + message_ids=None): + self.outbox = out + self.contents = contents + self._message_ids = message_ids or [] + self._messages = None + self.max_id = max_id or max(message_ids or [], default=None) + super().__init__(peer, self.max_id) @classmethod - def build(cls, update, others=None, self_id=None, *todo, **todo2): + def _build(cls, update, others=None, self_id=None, *todo, **todo2): if isinstance(update, _tl.UpdateReadHistoryInbox): return cls.Event(update.peer, update.max_id, False) elif isinstance(update, _tl.UpdateReadHistoryOutbox): @@ -54,90 +64,58 @@ class MessageRead(EventBuilder): message_ids=update.messages, contents=True) - def filter(self, event): - if self.inbox == event.outbox: - return - - return super().filter(event) - - class Event(EventCommon): + @property + def inbox(self): """ - Represents the event of one or more messages being read. - - Members: - max_id (`int`): - Up to which message ID has been read. Every message - with an ID equal or lower to it have been read. - - outbox (`bool`): - `True` if someone else has read your messages. - - contents (`bool`): - `True` if what was read were the contents of a message. - This will be the case when e.g. you play a voice note. - It may only be set on ``inbox`` events. + `True` if you have read someone else's messages. """ - def __init__(self, peer=None, max_id=None, out=False, contents=False, - message_ids=None): - self.outbox = out - self.contents = contents - self._message_ids = message_ids or [] - self._messages = None - self.max_id = max_id or max(message_ids or [], default=None) - super().__init__(peer, self.max_id) + return not self.outbox - @property - def inbox(self): - """ - `True` if you have read someone else's messages. - """ - return not self.outbox + @property + def message_ids(self): + """ + The IDs of the messages **which contents'** were read. - @property - def message_ids(self): - """ - The IDs of the messages **which contents'** were read. + Use :meth:`is_read` if you need to check whether a message + was read instead checking if it's in here. + """ + return self._message_ids - Use :meth:`is_read` if you need to check whether a message - was read instead checking if it's in here. - """ - return self._message_ids + async def get_messages(self): + """ + Returns the list of `Message ` + **which contents'** were read. - async def get_messages(self): - """ - Returns the list of `Message ` - **which contents'** were read. - - Use :meth:`is_read` if you need to check whether a message - was read instead checking if it's in here. - """ - if self._messages is None: - chat = await self.get_input_chat() - if not chat: - self._messages = [] - else: - self._messages = await self._client.get_messages( - chat, ids=self._message_ids) - - return self._messages - - def is_read(self, message): - """ - Returns `True` if the given message (or its ID) has been read. - - If a list-like argument is provided, this method will return a - list of booleans indicating which messages have been read. - """ - if utils.is_list_like(message): - return [(m if isinstance(m, int) else m.id) <= self.max_id - for m in message] + Use :meth:`is_read` if you need to check whether a message + was read instead checking if it's in here. + """ + if self._messages is None: + chat = await self.get_input_chat() + if not chat: + self._messages = [] else: - return (message if isinstance(message, int) - else message.id) <= self.max_id + self._messages = await self._client.get_messages( + chat, ids=self._message_ids) - def __contains__(self, message): - """`True` if the message(s) are read message.""" - if utils.is_list_like(message): - return all(self.is_read(message)) - else: - return self.is_read(message) + return self._messages + + def is_read(self, message): + """ + Returns `True` if the given message (or its ID) has been read. + + If a list-like argument is provided, this method will return a + list of booleans indicating which messages have been read. + """ + if utils.is_list_like(message): + return [(m if isinstance(m, int) else m.id) <= self.max_id + for m in message] + else: + return (message if isinstance(message, int) + else message.id) <= self.max_id + + def __contains__(self, message): + """`True` if the message(s) are read message.""" + if utils.is_list_like(message): + return all(self.is_read(message)) + else: + return self.is_read(message) diff --git a/telethon/_events/newmessage.py b/telethon/_events/newmessage.py index e4887002..e42aba7b 100644 --- a/telethon/_events/newmessage.py +++ b/telethon/_events/newmessage.py @@ -1,43 +1,43 @@ import re -from .common import EventBuilder, EventCommon, name_inner_event, _into_id_set +from .base import EventBuilder from .._misc import utils from .. import _tl from ..types import _custom -@name_inner_event -class NewMessage(EventBuilder): +class NewMessageEvent(EventBuilder, Message): """ - Occurs whenever a new text message or a message with media arrives. + Represents the event of a new message. This event can be treated + to all effects as a `Message `, + so please **refer to its documentation** to know what you can do + with this event. - Args: - incoming (`bool`, optional): - If set to `True`, only **incoming** messages will be handled. - Mutually exclusive with ``outgoing`` (can only set one of either). + Members: + message (`Message `): + This is the only difference with the received + `Message `, and will + return the `telethon.tl.custom.message.Message` itself, + not the text. - outgoing (`bool`, optional): - If set to `True`, only **outgoing** messages will be handled. - Mutually exclusive with ``incoming`` (can only set one of either). + See `Message ` for + the rest of available members and methods. - from_users (`entity`, optional): - Unlike `chats`, this parameter filters the *senders* of the - message. That is, only messages *sent by these users* will be - handled. Use `chats` if you want private messages with this/these - users. `from_users` lets you filter by messages sent by *one or - more* users across the desired chats (doesn't need a list). + pattern_match (`obj`): + The resulting object from calling the passed ``pattern`` function. + Here's an example using a string (defaults to regex match): - forwards (`bool`, optional): - Whether forwarded messages should be handled or not. By default, - both forwarded and normal messages are included. If it's `True` - *only* forwards will be handled. If it's `False` only messages - that are *not* forwards will be handled. - - pattern (`str`, `callable`, `Pattern`, optional): - If set, only messages matching this pattern will be handled. - You can specify a regex-like string which will be matched - against the message, a callable function that returns `True` - if a message is acceptable, or a compiled regex pattern. + >>> from telethon import TelegramClient, events + >>> client = TelegramClient(...) + >>> + >>> @client.on(events.NewMessage(pattern=r'hi (\\w+)!')) + ... async def handler(event): + ... # In this case, the result is a ``Match`` object + ... # since the `str` pattern was converted into + ... # the ``re.compile(pattern).match`` function. + ... print('Welcomed', event.pattern_match.group(1)) + ... + >>> Example .. code-block:: python @@ -57,45 +57,16 @@ class NewMessage(EventBuilder): await asyncio.sleep(5) await client.delete_messages(event.chat_id, [event.id, m.id]) """ - def __init__(self, chats=None, *, blacklist_chats=False, func=None, - incoming=None, outgoing=None, - from_users=None, forwards=None, pattern=None): - if incoming and outgoing: - incoming = outgoing = None # Same as no filter - elif incoming is not None and outgoing is None: - outgoing = not incoming - elif outgoing is not None and incoming is None: - incoming = not outgoing - elif all(x is not None and not x for x in (incoming, outgoing)): - raise ValueError("Don't create an event handler if you " - "don't want neither incoming nor outgoing!") + def __init__(self, message): + self.__dict__['_init'] = False + super().__init__(chat_peer=message.peer_id, + msg_id=message.id, broadcast=bool(message.post)) - super().__init__(chats, blacklist_chats=blacklist_chats, func=func) - self.incoming = incoming - self.outgoing = outgoing - self.from_users = from_users - self.forwards = forwards - if isinstance(pattern, str): - self.pattern = re.compile(pattern).match - elif not pattern or callable(pattern): - self.pattern = pattern - elif hasattr(pattern, 'match') and callable(pattern.match): - self.pattern = pattern.match - else: - raise TypeError('Invalid pattern type given') - - # Should we short-circuit? E.g. perform no check at all - self._no_check = all(x is None for x in ( - self.chats, self.incoming, self.outgoing, self.pattern, - self.from_users, self.forwards, self.from_users, self.func - )) - - async def _resolve(self, client): - await super()._resolve(client) - self.from_users = await _into_id_set(client, self.from_users) + self.pattern_match = None + self.message = message @classmethod - def build(cls, update, others, self_id, entities, client): + def _build(cls, update, others, self_id, entities, client): if isinstance(update, (_tl.UpdateNewMessage, _tl.UpdateNewChannelMessage)): if not isinstance(update.message, _tl.Message): @@ -139,85 +110,3 @@ class NewMessage(EventBuilder): return return cls.Event(_custom.Message._new(client, msg, entities, None)) - - def filter(self, event): - if self._no_check: - return event - - if self.incoming and event.message.out: - return - if self.outgoing and not event.message.out: - return - if self.forwards is not None: - if bool(self.forwards) != bool(event.message.fwd_from): - return - - if self.from_users is not None: - if event.message.sender_id not in self.from_users: - return - - if self.pattern: - match = self.pattern(event.message.message or '') - if not match: - return - event.pattern_match = match - - return super().filter(event) - - class Event(EventCommon): - """ - Represents the event of a new message. This event can be treated - to all effects as a `Message `, - so please **refer to its documentation** to know what you can do - with this event. - - Members: - message (`Message `): - This is the only difference with the received - `Message `, and will - return the `telethon.tl.custom.message.Message` itself, - not the text. - - See `Message ` for - the rest of available members and methods. - - pattern_match (`obj`): - The resulting object from calling the passed ``pattern`` function. - Here's an example using a string (defaults to regex match): - - >>> from telethon import TelegramClient, events - >>> client = TelegramClient(...) - >>> - >>> @client.on(events.NewMessage(pattern=r'hi (\\w+)!')) - ... async def handler(event): - ... # In this case, the result is a ``Match`` object - ... # since the `str` pattern was converted into - ... # the ``re.compile(pattern).match`` function. - ... print('Welcomed', event.pattern_match.group(1)) - ... - >>> - """ - def __init__(self, message): - self.__dict__['_init'] = False - super().__init__(chat_peer=message.peer_id, - msg_id=message.id, broadcast=bool(message.post)) - - self.pattern_match = None - self.message = message - - def _set_client(self, client): - super()._set_client(client) - m = self.message - self.__dict__['_init'] = True # No new attributes can be set - - def __getattr__(self, item): - if item in self.__dict__: - return self.__dict__[item] - else: - return getattr(self.message, item) - - def __setattr__(self, name, value): - if not self.__dict__['_init'] or name in self.__dict__: - self.__dict__[name] = value - else: - setattr(self.message, name, value) diff --git a/telethon/_events/raw.py b/telethon/_events/raw.py index 496f39e5..75b32de6 100644 --- a/telethon/_events/raw.py +++ b/telethon/_events/raw.py @@ -1,4 +1,4 @@ -from .common import EventBuilder +from .base import EventBuilder from .._misc import utils @@ -8,11 +8,6 @@ class Raw(EventBuilder): :tl:`Update` object that Telegram sends. You normally shouldn't need these. - Args: - types (`list` | `tuple` | `type`, optional): - The type or types that the :tl:`Update` instance must be. - Equivalent to ``if not isinstance(update, types): return``. - Example .. code-block:: python @@ -23,31 +18,6 @@ class Raw(EventBuilder): # Print all incoming updates print(update.stringify()) """ - def __init__(self, types=None, *, func=None): - super().__init__(func=func) - if not types: - self.types = None - elif not utils.is_list_like(types): - if not isinstance(types, type): - raise TypeError('Invalid input type given: {}'.format(types)) - - self.types = types - else: - if not all(isinstance(x, type) for x in types): - raise TypeError('Invalid input types given: {}'.format(types)) - - self.types = tuple(types) - - async def resolve(self, client): - self.resolved = True - @classmethod - def build(cls, update, others=None, self_id=None, *todo, **todo2): + def _build(cls, update, others=None, self_id=None, *todo, **todo2): return update - - def filter(self, event): - if not self.types or isinstance(event, self.types): - if self.func: - # Return the result of func directly as it may need to be awaited - return self.func(event) - return event diff --git a/telethon/_events/userupdate.py b/telethon/_events/userupdate.py index 35e8044c..e5c938e5 100644 --- a/telethon/_events/userupdate.py +++ b/telethon/_events/userupdate.py @@ -1,7 +1,7 @@ import datetime import functools -from .common import EventBuilder, EventCommon, name_inner_event +from .base import EventBuilder from .._misc import utils from .. import _tl from ..types import _custom @@ -32,11 +32,25 @@ def _requires_status(function): return wrapped -@name_inner_event -class UserUpdate(EventBuilder): +class UserUpdateEvent(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.SenderGetter): """ Occurs whenever a user goes online, starts typing, etc. + Members: + status (:tl:`UserStatus`, optional): + The user status if the update is about going online or offline. + + You should check this attribute first before checking any + of the seen within properties, since they will all be `None` + if the status is not set. + + action (:tl:`SendMessageAction`, optional): + The "typing" action if any the user is performing if any. + + You should check this attribute first before checking any + of the typing properties, since they will all be `None` + if the action is not set. + Example .. code-block:: python @@ -48,262 +62,242 @@ class UserUpdate(EventBuilder): if event.uploading: await client.send_message(event.user_id, 'What are you sending?') """ + def __init__(self, peer, *, status=None, chat_peer=None, typing=None): + _custom.chatgetter.ChatGetter.__init__(self, chat_peer or peer) + _custom.sendergetter.SenderGetter.__init__(self, utils.get_peer_id(peer)) + + self.status = status + self.action = typing + @classmethod - def build(cls, update, others=None, self_id=None, *todo, **todo2): + def _build(cls, update, others=None, self_id=None, *todo, **todo2): if isinstance(update, _tl.UpdateUserStatus): - return cls.Event(_tl.PeerUser(update.user_id), + return UserUpdateEvent(_tl.PeerUser(update.user_id), status=update.status) elif isinstance(update, _tl.UpdateChannelUserTyping): - return cls.Event(update.from_id, + return UserUpdateEvent(update.from_id, chat_peer=_tl.PeerChannel(update.channel_id), typing=update.action) elif isinstance(update, _tl.UpdateChatUserTyping): - return cls.Event(update.from_id, + return UserUpdateEvent(update.from_id, chat_peer=_tl.PeerChat(update.chat_id), typing=update.action) elif isinstance(update, _tl.UpdateUserTyping): - return cls.Event(update.user_id, + return UserUpdateEvent(update.user_id, typing=update.action) - class Event(EventCommon, _custom.sendergetter.SenderGetter): + def _set_client(self, client): + super()._set_client(client) + self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities) + + @property + def user(self): + """Alias for `sender `.""" + return self.sender + + async def get_user(self): + """Alias for `get_sender `.""" + return await self.get_sender() + + @property + def input_user(self): + """Alias for `input_sender `.""" + return self.input_sender + + async def get_input_user(self): + """Alias for `get_input_sender `.""" + return await self.get_input_sender() + + @property + def user_id(self): + """Alias for `sender_id `.""" + return self.sender_id + + @property + @_requires_action + def typing(self): """ - Represents the event of a user update - such as gone online, started typing, etc. - - Members: - status (:tl:`UserStatus`, optional): - The user status if the update is about going online or offline. - - You should check this attribute first before checking any - of the seen within properties, since they will all be `None` - if the status is not set. - - action (:tl:`SendMessageAction`, optional): - The "typing" action if any the user is performing if any. - - You should check this attribute first before checking any - of the typing properties, since they will all be `None` - if the action is not set. + `True` if the action is typing a message. """ - def __init__(self, peer, *, status=None, chat_peer=None, typing=None): - super().__init__(chat_peer or peer) - _custom.sendergetter.SenderGetter.__init__(self, utils.get_peer_id(peer)) + return isinstance(self.action, _tl.SendMessageTypingAction) - self.status = status - self.action = typing + @property + @_requires_action + def uploading(self): + """ + `True` if the action is uploading something. + """ + return isinstance(self.action, ( + _tl.SendMessageChooseContactAction, + _tl.SendMessageChooseStickerAction, + _tl.SendMessageUploadAudioAction, + _tl.SendMessageUploadDocumentAction, + _tl.SendMessageUploadPhotoAction, + _tl.SendMessageUploadRoundAction, + _tl.SendMessageUploadVideoAction + )) - def _set_client(self, client): - super()._set_client(client) - self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities) + @property + @_requires_action + def recording(self): + """ + `True` if the action is recording something. + """ + return isinstance(self.action, ( + _tl.SendMessageRecordAudioAction, + _tl.SendMessageRecordRoundAction, + _tl.SendMessageRecordVideoAction + )) - @property - def user(self): - """Alias for `sender `.""" - return self.sender + @property + @_requires_action + def playing(self): + """ + `True` if the action is playing a game. + """ + return isinstance(self.action, _tl.SendMessageGamePlayAction) - async def get_user(self): - """Alias for `get_sender `.""" - return await self.get_sender() + @property + @_requires_action + def cancel(self): + """ + `True` if the action was cancelling other actions. + """ + return isinstance(self.action, _tl.SendMessageCancelAction) - @property - def input_user(self): - """Alias for `input_sender `.""" - return self.input_sender + @property + @_requires_action + def geo(self): + """ + `True` if what's being uploaded is a geo. + """ + return isinstance(self.action, _tl.SendMessageGeoLocationAction) - async def get_input_user(self): - """Alias for `get_input_sender `.""" - return await self.get_input_sender() + @property + @_requires_action + def audio(self): + """ + `True` if what's being recorded/uploaded is an audio. + """ + return isinstance(self.action, ( + _tl.SendMessageRecordAudioAction, + _tl.SendMessageUploadAudioAction + )) - @property - def user_id(self): - """Alias for `sender_id `.""" - return self.sender_id + @property + @_requires_action + def round(self): + """ + `True` if what's being recorded/uploaded is a round video. + """ + return isinstance(self.action, ( + _tl.SendMessageRecordRoundAction, + _tl.SendMessageUploadRoundAction + )) - @property - @_requires_action - def typing(self): - """ - `True` if the action is typing a message. - """ - return isinstance(self.action, _tl.SendMessageTypingAction) + @property + @_requires_action + def video(self): + """ + `True` if what's being recorded/uploaded is an video. + """ + return isinstance(self.action, ( + _tl.SendMessageRecordVideoAction, + _tl.SendMessageUploadVideoAction + )) - @property - @_requires_action - def uploading(self): - """ - `True` if the action is uploading something. - """ - return isinstance(self.action, ( - _tl.SendMessageChooseContactAction, - _tl.SendMessageChooseStickerAction, - _tl.SendMessageUploadAudioAction, - _tl.SendMessageUploadDocumentAction, - _tl.SendMessageUploadPhotoAction, - _tl.SendMessageUploadRoundAction, - _tl.SendMessageUploadVideoAction - )) + @property + @_requires_action + def contact(self): + """ + `True` if what's being uploaded (selected) is a contact. + """ + return isinstance(self.action, _tl.SendMessageChooseContactAction) - @property - @_requires_action - def recording(self): - """ - `True` if the action is recording something. - """ - return isinstance(self.action, ( - _tl.SendMessageRecordAudioAction, - _tl.SendMessageRecordRoundAction, - _tl.SendMessageRecordVideoAction - )) + @property + @_requires_action + def document(self): + """ + `True` if what's being uploaded is document. + """ + return isinstance(self.action, _tl.SendMessageUploadDocumentAction) - @property - @_requires_action - def playing(self): - """ - `True` if the action is playing a game. - """ - return isinstance(self.action, _tl.SendMessageGamePlayAction) + @property + @_requires_action + def sticker(self): + """ + `True` if what's being uploaded is a sticker. + """ + return isinstance(self.action, _tl.SendMessageChooseStickerAction) - @property - @_requires_action - def cancel(self): - """ - `True` if the action was cancelling other actions. - """ - return isinstance(self.action, _tl.SendMessageCancelAction) + @property + @_requires_action + def photo(self): + """ + `True` if what's being uploaded is a photo. + """ + return isinstance(self.action, _tl.SendMessageUploadPhotoAction) - @property - @_requires_action - def geo(self): - """ - `True` if what's being uploaded is a geo. - """ - return isinstance(self.action, _tl.SendMessageGeoLocationAction) + @property + @_requires_action + def last_seen(self): + """ + Exact `datetime.datetime` when the user was last seen if known. + """ + if isinstance(self.status, _tl.UserStatusOffline): + return self.status.was_online - @property - @_requires_action - def audio(self): - """ - `True` if what's being recorded/uploaded is an audio. - """ - return isinstance(self.action, ( - _tl.SendMessageRecordAudioAction, - _tl.SendMessageUploadAudioAction - )) + @property + @_requires_status + def until(self): + """ + The `datetime.datetime` until when the user should appear online. + """ + if isinstance(self.status, _tl.UserStatusOnline): + return self.status.expires - @property - @_requires_action - def round(self): - """ - `True` if what's being recorded/uploaded is a round video. - """ - return isinstance(self.action, ( - _tl.SendMessageRecordRoundAction, - _tl.SendMessageUploadRoundAction - )) + def _last_seen_delta(self): + if isinstance(self.status, _tl.UserStatusOffline): + return datetime.datetime.now(tz=datetime.timezone.utc) - self.status.was_online + elif isinstance(self.status, _tl.UserStatusOnline): + return datetime.timedelta(days=0) + elif isinstance(self.status, _tl.UserStatusRecently): + return datetime.timedelta(days=1) + elif isinstance(self.status, _tl.UserStatusLastWeek): + return datetime.timedelta(days=7) + elif isinstance(self.status, _tl.UserStatusLastMonth): + return datetime.timedelta(days=30) + else: + return datetime.timedelta(days=365) - @property - @_requires_action - def video(self): - """ - `True` if what's being recorded/uploaded is an video. - """ - return isinstance(self.action, ( - _tl.SendMessageRecordVideoAction, - _tl.SendMessageUploadVideoAction - )) + @property + @_requires_status + def online(self): + """ + `True` if the user is currently online, + """ + return self._last_seen_delta() <= datetime.timedelta(days=0) - @property - @_requires_action - def contact(self): - """ - `True` if what's being uploaded (selected) is a contact. - """ - return isinstance(self.action, _tl.SendMessageChooseContactAction) + @property + @_requires_status + def recently(self): + """ + `True` if the user was seen within a day. + """ + return self._last_seen_delta() <= datetime.timedelta(days=1) - @property - @_requires_action - def document(self): - """ - `True` if what's being uploaded is document. - """ - return isinstance(self.action, _tl.SendMessageUploadDocumentAction) + @property + @_requires_status + def within_weeks(self): + """ + `True` if the user was seen within 7 days. + """ + return self._last_seen_delta() <= datetime.timedelta(days=7) - @property - @_requires_action - def sticker(self): - """ - `True` if what's being uploaded is a sticker. - """ - return isinstance(self.action, _tl.SendMessageChooseStickerAction) - - @property - @_requires_action - def photo(self): - """ - `True` if what's being uploaded is a photo. - """ - return isinstance(self.action, _tl.SendMessageUploadPhotoAction) - - @property - @_requires_action - def last_seen(self): - """ - Exact `datetime.datetime` when the user was last seen if known. - """ - if isinstance(self.status, _tl.UserStatusOffline): - return self.status.was_online - - @property - @_requires_status - def until(self): - """ - The `datetime.datetime` until when the user should appear online. - """ - if isinstance(self.status, _tl.UserStatusOnline): - return self.status.expires - - def _last_seen_delta(self): - if isinstance(self.status, _tl.UserStatusOffline): - return datetime.datetime.now(tz=datetime.timezone.utc) - self.status.was_online - elif isinstance(self.status, _tl.UserStatusOnline): - return datetime.timedelta(days=0) - elif isinstance(self.status, _tl.UserStatusRecently): - return datetime.timedelta(days=1) - elif isinstance(self.status, _tl.UserStatusLastWeek): - return datetime.timedelta(days=7) - elif isinstance(self.status, _tl.UserStatusLastMonth): - return datetime.timedelta(days=30) - else: - return datetime.timedelta(days=365) - - @property - @_requires_status - def online(self): - """ - `True` if the user is currently online, - """ - return self._last_seen_delta() <= datetime.timedelta(days=0) - - @property - @_requires_status - def recently(self): - """ - `True` if the user was seen within a day. - """ - return self._last_seen_delta() <= datetime.timedelta(days=1) - - @property - @_requires_status - def within_weeks(self): - """ - `True` if the user was seen within 7 days. - """ - return self._last_seen_delta() <= datetime.timedelta(days=7) - - @property - @_requires_status - def within_months(self): - """ - `True` if the user was seen within 30 days. - """ - return self._last_seen_delta() <= datetime.timedelta(days=30) + @property + @_requires_status + def within_months(self): + """ + `True` if the user was seen within 30 days. + """ + return self._last_seen_delta() <= datetime.timedelta(days=30) From 0802f7e6b21a9e0a2f99d37647e7a6d00233738a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 28 Jan 2022 14:12:32 +0100 Subject: [PATCH 179/256] Rework methods to manage event handlers --- readthedocs/misc/v2-migration-guide.rst | 1 + telethon/_client/telegrambaseclient.py | 1 + telethon/_client/telegramclient.py | 100 +++++++++++++++++----- telethon/_client/updates.py | 108 +++++++++++++++++------- telethon/_events/base.py | 21 +++++ telethon/_events/newmessage.py | 2 +- telethon/_events/userupdate.py | 2 +- telethon/events.py | 3 +- 8 files changed, 184 insertions(+), 54 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index ce077d13..c473ac76 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -788,3 +788,4 @@ it's now this also means filters are unified, although not all have an effect on all events. from_users renamed to senders. messageread inbox is gone in favor of outgoing/incoming. events.register, unregister, is_handler and list are gone. now you can typehint instead. def handler(event: events.NewMessage) +client.on, add, and remove have changed parameters/retval \ No newline at end of file diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 92dc1b65..277fd6cf 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -148,6 +148,7 @@ def init( self._no_updates = not receive_updates self._updates_queue = asyncio.Queue(maxsize=max_queued_updates) self._updates_handle = None + self._update_handlers = [] # sorted list self._message_box = MessageBox() self._entity_cache = EntityCache() # required for proper update handling (to know when to getDifference) diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 136a6d31..851f39d1 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -2791,15 +2791,24 @@ class TelegramClient: """ @forward_call(updates.on) - def on(self: 'TelegramClient', event: EventBuilder): + def on(self: 'TelegramClient', *events, priority=0, **filters): """ Decorator used to `add_event_handler` more conveniently. + This decorator should be above other decorators which modify the function. Arguments - event (`_EventBuilder` | `type`): - The event builder class or instance to be used, - for instance ``events.NewMessage``. + event (`type` | `tuple`): + The event type(s) you wish to receive, for instance ``events.NewMessage``. + This may also be raw update types. + The same handler is registered multiple times, one per type. + + priority (`int`): + The event priority. Events with higher priority are dispatched first. + The order between events with the same priority is arbitrary. + + filters (any): + Filters passed to `make_filter`. Example .. code-block:: python @@ -2808,7 +2817,12 @@ class TelegramClient: client = TelegramClient(...) # Here we use client.on - @client.on(events.NewMessage) + @client.on(events.NewMessage, priority=100) + async def handler(event): + ... + + # Both new incoming messages and incoming edits + @client.on(events.NewMessage, events.MessageEdited, incoming=True) async def handler(event): ... """ @@ -2816,8 +2830,11 @@ class TelegramClient: @forward_call(updates.add_event_handler) def add_event_handler( self: 'TelegramClient', - callback: updates.Callback, - event: EventBuilder = None): + callback: updates.Callback = None, + event: EventBuilder = None, + priority=0, + **filters + ): """ Registers a new event handler callback. @@ -2827,17 +2844,29 @@ class TelegramClient: callback (`callable`): The callable function accepting one parameter to be used. - Note that if you have used `telethon.events.register` in - the callback, ``event`` will be ignored, and instead the - events you previously registered will be used. + If `None`, the method can be used as a decorator. Note that the handler function + will be replaced with the `EventHandler` instance in this case, but it will still + be callable. event (`_EventBuilder` | `type`, optional): The event builder class or instance to be used, for instance ``events.NewMessage``. - If left unspecified, `telethon.events.raw.Raw` (the - :tl:`Update` objects with no further processing) will - be passed instead. + If left unspecified, it will be inferred from the type hint + used in the handler, or be `telethon.events.raw.Raw` (the + :tl:`Update` objects with no further processing) if there is + none. Note that the type hint must be the desired type. It + cannot be a string, an union, or anything more complex. + + priority (`int`): + The event priority. Events with higher priority are dispatched first. + The order between events with the same priority is arbitrary. + + filters (any): + Filters passed to `make_filter`. + + Returns + An `EventHandler` instance, which can be used Example .. code-block:: python @@ -2845,22 +2874,47 @@ class TelegramClient: from telethon import TelegramClient, events client = TelegramClient(...) + # Adding a handler, the "boring" way async def handler(event): ... - client.add_event_handler(handler, events.NewMessage) + client.add_event_handler(handler, events.NewMessage, priority=50) + + # Automatic type + async def handler(event: events.MessageEdited) + ... + + client.add_event_handler(handler, outgoing=False) + + # Streamlined adding + @client.add_event_handler + async def handler(event: events.MessageDeleted): + ... """ @forward_call(updates.remove_event_handler) def remove_event_handler( self: 'TelegramClient', - callback: updates.Callback, - event: EventBuilder = None) -> int: + callback: updates.Callback = None, + event: EventBuilder = None, + priority=None, + ) -> int: """ Inverse operation of `add_event_handler()`. If no event is given, all events for this callback are removed. - Returns how many callbacks were removed. + Returns a list in arbitrary order with all removed `EventHandler` instances. + + Arguments + callback (`callable`): + The callable function accepting one parameter to be used. + If passed an `EventHandler` instance, both `event` and `priority` are ignored. + + event (`_EventBuilder` | `type`, optional): + The event builder class or instance to be used when searching. + + priority (`int`): + The event priority to be used when searching. Example .. code-block:: python @@ -2876,6 +2930,12 @@ class TelegramClient: # "handler" will stop receiving anything client.remove_event_handler(handler) + + # Remove all handlers with priority 50 + client.remove_event_handler(priority=50) + + # Remove all deleted-message handlers + client.remove_event_handler(event=events.MessageDeleted) """ @forward_call(updates.list_event_handlers) @@ -2885,7 +2945,7 @@ class TelegramClient: Lists all registered event handlers. Returns - A list of pairs consisting of ``(callback, event)``. + A list of all registered `EventHandler` in arbitrary order. Example .. code-block:: python @@ -2895,8 +2955,8 @@ class TelegramClient: '''Greets someone''' await event.reply('Hi') - for callback, event in client.list_event_handlers(): - print(id(callback), type(event)) + for handler in client.list_event_handlers(): + print(id(handler.callback), handler.event) """ @forward_call(updates.catch_up) diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index a9875bba..70c0c094 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -7,12 +7,15 @@ import time import traceback import typing import logging +import inspect +import bisect +import warnings from collections import deque from ..errors._rpcbase import RpcError -from .._events.base import EventBuilder from .._events.raw import Raw -from .._events.base import StopPropagation, _get_handlers +from .._events.base import StopPropagation, EventBuilder, EventHandler +from .._events.filters import make_filter from .._misc import utils from .. import _tl @@ -33,51 +36,96 @@ async def run_until_disconnected(self: 'TelegramClient'): await self(_tl.fn.updates.GetState()) await self._sender.wait_disconnected() -def on(self: 'TelegramClient', event: EventBuilder): +def on(self: 'TelegramClient', *events, priority=0, **filters): def decorator(f): - self.add_event_handler(f, event) + for event in events: + self.add_event_handler(f, event, priority=priority, **filters) return f return decorator def add_event_handler( self: 'TelegramClient', - callback: Callback, - event: EventBuilder = None): - builders = _get_handlers(callback) - if builders is not None: - for event in builders: - self._event_builders.append((event, callback)) - return + callback=None, + event=None, + priority=0, + **filters +): + if callback is None: + return functools.partial(add_event_handler, self, event=event, priority=priority, **filters) - if isinstance(event, type): - event = event() - elif not event: - event = Raw() + if event is None: + for param in inspect.signature(callback).parameters.values(): + if not issubclass(param.annotation, EventBuilder): + raise TypeError(f'unrecognized event handler type: {param.annotation!r}') + event = param.annotation + break # only check the first parameter - self._event_builders.append((event, callback)) + if event is None: + event = Raw + + handler = EventHandler(event, callback, priority, make_filter(**filters)) + bisect.insort(self._update_handlers, handler) + return handler def remove_event_handler( self: 'TelegramClient', - callback: Callback, - event: EventBuilder = None) -> int: - found = 0 - if event and not isinstance(event, type): - event = type(event) + callback, + event, + priority, +): + if callback is None and event is None and priority is None: + raise ValueError('must specify at least one of callback, event or priority') - i = len(self._event_builders) - while i: - i -= 1 - ev, cb = self._event_builders[i] - if cb == callback and (not event or isinstance(ev, event)): - del self._event_builders[i] - found += 1 + if not self._update_handlers: + return [] # won't be removing anything (some code paths rely on non-empty lists) - return found + if isinstance(callback, EventHandler): + if event is not None or priority is not None: + warnings.warn('event and priority are ignored when removing EventHandler instances') + + index = bisect.bisect_left(self._update_handlers, callback) + try: + if self._update_handlers[index] == callback: + return [self._update_handlers.pop(index)] + except IndexError: + pass + return [] + + if priority is not None: + # can binary-search (using a dummy EventHandler) + index = bisect.bisect_right(self._update_handlers, EventHandler(None, None, priority, None)) + try: + while self._update_handlers[index].priority == priority: + index += 1 + except IndexError: + pass + + removed = [] + while index > 0 and self._update_handlers[index - 1].priority == priority: + index -= 1 + if callback is not None and self._update_handlers[index].callback != callback: + continue + if event is not None and self._update_handlers[index].event != event: + continue + removed.append(self._update_handlers.pop(index)) + + return removed + + # slow-path, remove all matching + removed = [] + for index, handler in reversed(enumerate(self._update_handlers)): + if callback is not None and handler.callback != callback: + continue + if event is not None and handler.event != event: + continue + removed.append(self._update_handlers.pop(index)) + + return removed def list_event_handlers(self: 'TelegramClient')\ -> 'typing.Sequence[typing.Tuple[Callback, EventBuilder]]': - return [(callback, event) for event, callback in self._event_builders] + return self._update_handlers[:] async def catch_up(self: 'TelegramClient'): # The update loop is probably blocked on either timeout or an update to arrive. diff --git a/telethon/_events/base.py b/telethon/_events/base.py index 303c5976..1964b22f 100644 --- a/telethon/_events/base.py +++ b/telethon/_events/base.py @@ -1,4 +1,5 @@ import abc +import functools class StopPropagation(Exception): @@ -41,3 +42,23 @@ class EventBuilder(abc.ABC): `self_id` should be the current user's ID, since it is required for some events which lack this information but still need it. """ + + +@functools.total_ordering +class EventHandler: + __slots__ = ('_event', '_callback', '_priority', '_filter') + + def __init__(self, event, callback, priority, filter): + self._event = event + self._callback = callback + self._priority = priority + self._filter = filter + + def __eq__(self, other): + return self is other + + def __lt__(self, other): + return self._priority < other._priority + + def __call__(self, *args, **kwargs): + return self._callback(*args, **kwargs) diff --git a/telethon/_events/newmessage.py b/telethon/_events/newmessage.py index e42aba7b..ee62c655 100644 --- a/telethon/_events/newmessage.py +++ b/telethon/_events/newmessage.py @@ -6,7 +6,7 @@ from .. import _tl from ..types import _custom -class NewMessageEvent(EventBuilder, Message): +class NewMessage(EventBuilder, _custom.Message): """ Represents the event of a new message. This event can be treated to all effects as a `Message `, diff --git a/telethon/_events/userupdate.py b/telethon/_events/userupdate.py index e5c938e5..b9af86e7 100644 --- a/telethon/_events/userupdate.py +++ b/telethon/_events/userupdate.py @@ -32,7 +32,7 @@ def _requires_status(function): return wrapped -class UserUpdateEvent(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.SenderGetter): +class UserUpdate(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.SenderGetter): """ Occurs whenever a user goes online, starts typing, etc. diff --git a/telethon/events.py b/telethon/events.py index 5dca03ae..125e6b81 100644 --- a/telethon/events.py +++ b/telethon/events.py @@ -1,6 +1,5 @@ -from ._events.base import StopPropagation, register, unregister, is_handler, list +from ._events.base import StopPropagation from ._events.raw import Raw - from ._events.album import Album from ._events.chataction import ChatAction from ._events.messagedeleted import MessageDeleted From 05457eaf1c48c7bdfce3a74f435e8b1d8407e8fd Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 28 Jan 2022 20:20:37 +0100 Subject: [PATCH 180/256] Fix date usage in messagebox impl --- telethon/_updates/messagebox.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/telethon/_updates/messagebox.py b/telethon/_updates/messagebox.py index 1562cd21..d2bdd174 100644 --- a/telethon/_updates/messagebox.py +++ b/telethon/_updates/messagebox.py @@ -17,6 +17,8 @@ While there are entries for which their difference must be fetched, to get the difference. """ import asyncio +import datetime +import time from dataclasses import dataclass, field from .._sessions.types import SessionState, ChannelState from .. import _tl @@ -117,7 +119,7 @@ class MessageBox: map: dict = field(default_factory=dict) # entry -> state # Additional fields beyond PTS needed by `ENTRY_ACCOUNT`. - date: int = 1 + date: datetime.datetime = datetime.datetime(*time.gmtime(0)[:6]).replace(tzinfo=datetime.timezone.utc) seq: int = NO_SEQ # Holds the entry with the closest deadline (optimization to avoid recalculating the minimum deadline). @@ -154,7 +156,7 @@ class MessageBox: self.map[ENTRY_SECRET] = State(pts=session_state.qts, deadline=deadline) self.map.update((s.channel_id, State(pts=s.pts, deadline=deadline)) for s in channel_states) - self.date = session_state.date + self.date = datetime.datetime.fromtimestamp(session_state.date).replace(tzinfo=datetime.timezone.utc) self.seq = session_state.seq self.next_deadline = ENTRY_ACCOUNT @@ -167,7 +169,7 @@ class MessageBox: return dict( pts=self.map[ENTRY_ACCOUNT].pts if ENTRY_ACCOUNT in self.map else NO_SEQ, qts=self.map[ENTRY_SECRET].pts if ENTRY_SECRET in self.map else NO_SEQ, - date=self.date, + date=int(self.date.timestamp()), seq=self.seq, ), {id: state.pts for id, state in self.map.items() if isinstance(id, int)} From a16c397de4e7f713774eb3cd104e0bd642fad419 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 28 Jan 2022 20:20:53 +0100 Subject: [PATCH 181/256] Fix saving entity types in sqlite ty is already a subclass of int. --- telethon/_sessions/sqlite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/_sessions/sqlite.py b/telethon/_sessions/sqlite.py index b41975fb..2ea419be 100644 --- a/telethon/_sessions/sqlite.py +++ b/telethon/_sessions/sqlite.py @@ -245,7 +245,7 @@ class SQLiteSession(Session): try: c.executemany( 'insert or replace into entity values (?,?,?)', - [(e.id, e.access_hash, e.ty.value) for e in entities] + [(e.id, e.access_hash, e.ty) for e in entities] ) finally: c.close() From a1fe80557af559f42022a86266fff0e03b1adde0 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 28 Jan 2022 20:21:15 +0100 Subject: [PATCH 182/256] Implement dispatching events --- telethon/_client/telegrambaseclient.py | 1 + telethon/_client/updates.py | 69 ++++++++++++++++++++++++-- telethon/_events/base.py | 4 +- 3 files changed, 69 insertions(+), 5 deletions(-) diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 277fd6cf..8af7ec52 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -149,6 +149,7 @@ def init( self._updates_queue = asyncio.Queue(maxsize=max_queued_updates) self._updates_handle = None self._update_handlers = [] # sorted list + self._dispatching_update_handlers = False # while dispatching, if add/remove are called, we need to make a copy self._message_box = MessageBox() self._entity_cache = EntityCache() # required for proper update handling (to know when to getDifference) diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index 70c0c094..c0ba9fa9 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -56,15 +56,42 @@ def add_event_handler( if event is None: for param in inspect.signature(callback).parameters.values(): - if not issubclass(param.annotation, EventBuilder): - raise TypeError(f'unrecognized event handler type: {param.annotation!r}') event = param.annotation break # only check the first parameter if event is None: event = Raw + if not inspect.iscoroutinefunction(callback): + raise TypeError(f'callback was not an async def function: {callback!r}') + + if not isinstance(event, type): + raise TypeError(f'event type was not a type (an instance of something was probably used): {event!r}') + + if not isinstance(priority, int): + raise TypeError(f'priority was not an integer: {priority!r}') + + if not issubclass(event, EventBuilder): + try: + if event.SUBCLASS_OF_ID != 0x9f89304e: + raise TypeError(f'invalid raw update type for the event handler: {event!r}') + + if 'types' in filters: + warnings.warn('"types" filter is ignored when the event type already is a raw update') + + filters['types'] = event + event = Raw + except AttributeError: + raise TypeError(f'unrecognized event handler type: {param.annotation!r}') + handler = EventHandler(event, callback, priority, make_filter(**filters)) + + if self._dispatching_update_handlers: + # Now that there's a copy, we're no longer dispatching from the old update_handlers, + # so we can modify it. This is why we can turn the flag off. + self._update_handlers = self._update_handlers[:] + self._dispatching_update_handlers = False + bisect.insort(self._update_handlers, handler) return handler @@ -80,6 +107,11 @@ def remove_event_handler( if not self._update_handlers: return [] # won't be removing anything (some code paths rely on non-empty lists) + if self._dispatching_update_handlers: + # May be an unnecessary copy if nothing was removed, but that's not a big deal. + self._update_handlers = self._update_handlers[:] + self._dispatching_update_handlers = False + if isinstance(callback, EventHandler): if event is not None or priority is not None: warnings.warn('event and priority are ignored when removing EventHandler instances') @@ -138,8 +170,7 @@ async def _update_loop(self: 'TelegramClient'): updates_to_dispatch = deque() while self.is_connected(): if updates_to_dispatch: - # TODO dispatch - updates_to_dispatch.popleft() + await _dispatch(self, updates_to_dispatch.popleft()) continue get_diff = self._message_box.get_difference() @@ -176,3 +207,33 @@ async def _update_loop(self: 'TelegramClient'): updates_to_dispatch.extend(processed) except Exception: self._log[__name__].exception('Fatal error handling updates (this is a bug in Telethon, please report it)') + + +async def _dispatch(self, update): + self._dispatching_update_handlers = True + + event_cache = {} + for handler in self._update_handlers: + event = event_cache.get(handler._event) + if not event: + event_cache[handler._event] = event = handler._event._build(update) + + while True: + # filters can be modified at any time, and there can be any amount of them which are not yet resolved + try: + if handler._filter(event): + try: + await handler._callback(event) + except Exception: + name = getattr(handler.callback, '__name__', repr(handler.callback)) + self._log[__name__].exception('Unhandled exception on %s (this is likely a bug in your code)', name) + break + except NotResolved as e: + try: + await unresolved.resolve() + except Exception: + # we cannot really do much about this; it might be a temporary network issue + warnings.warn(f'failed to resolve filter, handler will be skipped: {e.unresolved!r}') + break + + self._dispatching_update_handlers = False diff --git a/telethon/_events/base.py b/telethon/_events/base.py index 1964b22f..f0af195f 100644 --- a/telethon/_events/base.py +++ b/telethon/_events/base.py @@ -1,6 +1,8 @@ import abc import functools +from .filters import Filter + class StopPropagation(Exception): """ @@ -48,7 +50,7 @@ class EventBuilder(abc.ABC): class EventHandler: __slots__ = ('_event', '_callback', '_priority', '_filter') - def __init__(self, event, callback, priority, filter): + def __init__(self, event: EventBuilder, callback: callable, priority: int, filter: Filter): self._event = event self._callback = callback self._priority = priority From 055eca49ea7585f2298e68780d098f01fd5d2a2e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 28 Jan 2022 21:06:19 +0100 Subject: [PATCH 183/256] Introduce some filters --- telethon/_events/filters/__init__.py | 150 +++++++++++++++++++++++++++ telethon/_events/filters/base.py | 78 ++++++++++++++ telethon/_events/filters/entities.py | 25 +++++ telethon/_events/filters/generic.py | 13 +++ telethon/_events/filters/messages.py | 26 +++++ 5 files changed, 292 insertions(+) create mode 100644 telethon/_events/filters/__init__.py create mode 100644 telethon/_events/filters/base.py create mode 100644 telethon/_events/filters/entities.py create mode 100644 telethon/_events/filters/generic.py create mode 100644 telethon/_events/filters/messages.py diff --git a/telethon/_events/filters/__init__.py b/telethon/_events/filters/__init__.py new file mode 100644 index 00000000..0b0df5a3 --- /dev/null +++ b/telethon/_events/filters/__init__.py @@ -0,0 +1,150 @@ +from .base import Filter, And, Or, Not, Identity, Always, Never +from .generic import Types +from .entities import Chats, Senders +from .messages import Incoming, Outgoing, Pattern, Data + + +_sentinel = object() + + +def make_filter( + chats=_sentinel, + blacklist_chats=_sentinel, + func=_sentinel, + types=_sentinel, + incoming=_sentinel, + outgoing=_sentinel, + senders=_sentinel, + blacklist_senders=_sentinel, + forwards=_sentinel, + pattern=_sentinel, + data=_sentinel, +): + """ + Create a new `And` filter joining all the filters specified as input parameters. + + Not all filters may have an effect on all events. + + chats (`entity`, optional): + May be one or more entities (username/peer/etc.), preferably IDs. + By default, only matching chats will be handled. + + blacklist_chats (`bool`, optional): + Whether to treat the chats as a blacklist instead of + as a whitelist (default). This means that every chat + will be handled *except* those specified in ``chats`` + which will be ignored if ``blacklist_chats=True``. + + func (`callable`, optional): + A callable (async or not) function that should accept the event as input + parameter, and return a value indicating whether the event + should be dispatched or not (any truthy value will do, it + does not need to be a `bool`). It works like a custom filter: + + .. code-block:: python + + @client.on(events.NewMessage(func=lambda e: e.is_private)) + async def handler(event): + pass # code here + + incoming (`bool`, optional): + If set to `True`, only **incoming** messages will be handled. + If set to `False`, incoming messages will be ignored. + If both incoming are outgoing are set, whichever is true will be handled. + + outgoing (`bool`, optional): + If set to `True`, only **outgoing** messages will be handled. + If set to `False`, outgoing messages will be ignored. + If both incoming are outgoing are set, whichever is true will be handled. + + senders (`entity`, optional): + Unlike `chats`, this parameter filters the *senders* of the + message. That is, only messages *sent by these users* will be + handled. Use `chats` if you want private messages with this/these + users. `senders` lets you filter by messages sent by *one or + more* users across the desired chats (doesn't need a list). + + blacklist_senders (`bool`): + Whether to treat the senders as a blacklist instead of + as a whitelist (default). This means that every sender + will be handled *except* those specified in ``senders`` + which will be ignored if ``blacklist_senders=True``. + + forwards (`bool`, optional): + Whether forwarded messages should be handled or not. By default, + both forwarded and normal messages are included. If it's `True` + *only* forwards will be handled. If it's `False` only messages + that are *not* forwards will be handled. + + pattern (`str`, `callable`, `Pattern`, optional): + If set, only messages matching this pattern will be handled. + You can specify a regex-like string which will be matched + against the message, a callable function that returns `True` + if a message is acceptable, or a compiled regex pattern. + + data (`bytes`, `str`, `callable`, optional): + If set, the inline button payload data must match this data. + A UTF-8 string can also be given, a regex or a callable. For + instance, to check against ``'data_1'`` and ``'data_2'`` you + can use ``re.compile(b'data_')``. + + types (`list` | `tuple` | `type`, optional): + The type or types that the :tl:`Update` instance must be. + Equivalent to ``if not isinstance(update, types): return``. + """ + filters = [] + + if chats is not _sentinel: + f = Chats(chats) + if blacklist_chats is not _sentinel and blacklist_chats: + f = Not(f) + filters.append(f) + + if func is not _sentinel: + filters.append(Identity(func)) + + if types is not _sentinel: + filters.append(Types(types)) + + if incoming is not _sentinel: + if outgoing is not _sentinel: + if incoming and outgoing: + pass # no need to filter + elif incoming: + filters.append(Incoming()) + elif outgoing: + filters.append(Outgoing()) + else: + return Never() # why? + elif incoming: + filters.append(Incoming()) + else: + filters.append(Outgoing()) + elif outgoing is not _sentinel: + if outgoing: + filters.append(Outgoing()) + else: + filters.append(Incoming()) + + if senders is not _sentinel: + f = Senders(senders) + if blacklist_senders is not _sentinel and blacklist_senders: + f = Not(f) + filters.append(f) + + if forwards is not _sentinel: + filters.append(Forward()) + + if pattern is not _sentinel: + filters.append(Pattern(pattern)) + + if data is not _sentinel: + filters.append(Data(data)) + + return And(*filters) if filters else Always() + + +class NotResolved(ValueError): + def __init__(self, unresolved): + super().__init__() + self.unresolved = unresolved diff --git a/telethon/_events/filters/base.py b/telethon/_events/filters/base.py new file mode 100644 index 00000000..b8a8d9ca --- /dev/null +++ b/telethon/_events/filters/base.py @@ -0,0 +1,78 @@ +import abc + + +class Filter(abc.ABC): + @abc.abstractmethod + def __call__(self, event): + return True + + def __and__(self, other): + return And(self, other) + + def __or__(self, other): + return Or(self, other) + + def __invert__(self): + return Not(self) + + +class And(Filter): + """ + All underlying filters must return `True` for this filter to be `True`. + """ + def __init__(self, *filters): + self._filters = filters + + def __call__(self, event): + return all(f(event) for f in self._filters) + + +class Or(Filter): + """ + At least one underlying filter must return `True` for this filter to be `True`. + """ + def __init__(self, *filters): + self._filters = filters + + def __call__(self, event): + return any(f(event) for f in self._filters) + + +class Not(Filter): + """ + The underlying filter must return `False` for this filter to be `True`. + """ + def __init__(self, filter): + self._filter = filter + + def __call__(self, event): + return not self._filter(event) + + +class Identity(Filter): + """ + Return the value of the underlying filter (or callable) without any modifications. + """ + def __init__(self, filter): + self._filter = filter + + def __call__(self, event): + return self._filter(event) + + +class Always(Filter): + """ + This filter always returns `True`, and is used as the "empty filter". + """ + def __call__(self, event): + return True + + +class Never(Filter): + """ + This filter always returns `False`, and is used when an impossible filter is made + (for example, neither outgoing nor incoming is always false). This can be used to + "turn off" handlers without removing them. + """ + def __call__(self, event): + return False diff --git a/telethon/_events/filters/entities.py b/telethon/_events/filters/entities.py new file mode 100644 index 00000000..4ccb3569 --- /dev/null +++ b/telethon/_events/filters/entities.py @@ -0,0 +1,25 @@ +from .base import Filter + + +class Chats: + """ + The update type must match the specified instances for the filter to return `True`. + This is most useful for raw API. + """ + def __init__(self, types): + self._types = types + + def __call__(self, event): + return isinstance(event, self._types) + + +class Senders: + """ + The update type must match the specified instances for the filter to return `True`. + This is most useful for raw API. + """ + def __init__(self, types): + self._types = types + + def __call__(self, event): + return isinstance(event, self._types) diff --git a/telethon/_events/filters/generic.py b/telethon/_events/filters/generic.py new file mode 100644 index 00000000..c3e010c6 --- /dev/null +++ b/telethon/_events/filters/generic.py @@ -0,0 +1,13 @@ +from .base import Filter + + +class Types: + """ + The update type must match the specified instances for the filter to return `True`. + This is most useful for raw API. + """ + def __init__(self, types): + self._types = types + + def __call__(self, event): + return isinstance(event, self._types) diff --git a/telethon/_events/filters/messages.py b/telethon/_events/filters/messages.py new file mode 100644 index 00000000..a6bacbd6 --- /dev/null +++ b/telethon/_events/filters/messages.py @@ -0,0 +1,26 @@ +import re +from .base import Filter + + +class Pattern: + """ + The update type must match the specified instances for the filter to return `True`. + This is most useful for raw API. + """ + def __init__(self, pattern): + self._pattern = re.compile(pattern).match + + def __call__(self, event): + return self._pattern(event.text) + + +class Data: + """ + The update type must match the specified instances for the filter to return `True`. + This is most useful for raw API. + """ + def __init__(self, data): + self._data = re.compile(data).match + + def __call__(self, event): + return self._data(event.data) From a3a60e6dca8dfd7578755b63099a068072ff4899 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 28 Jan 2022 21:06:31 +0100 Subject: [PATCH 184/256] Add proper warnings when applying filters to updates fails --- telethon/_client/updates.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index c0ba9fa9..2477af9c 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -227,13 +227,18 @@ async def _dispatch(self, update): except Exception: name = getattr(handler.callback, '__name__', repr(handler.callback)) self._log[__name__].exception('Unhandled exception on %s (this is likely a bug in your code)', name) - break - except NotResolved as e: + except NotResolved as nr: try: - await unresolved.resolve() - except Exception: + await nr.unresolved.resolve() + continue + except Exception as e: # we cannot really do much about this; it might be a temporary network issue - warnings.warn(f'failed to resolve filter, handler will be skipped: {e.unresolved!r}') - break + warnings.warn(f'failed to resolve filter, handler will be skipped: {e}: {nr.unresolved!r}') + except Exception as e: + # invalid filter (e.g. types when types were not used as input) + warnings.warn(f'invalid filter applied, handler will be skipped: {e}: {e.filter!r}') + + # we only want to continue on unresolved filter (to check if there are more unresolved) + break self._dispatching_update_handlers = False From acc512683c107f80d47ab0fdaac0d1ac9439d43c Mon Sep 17 00:00:00 2001 From: Devesh Pal Date: Fri, 4 Feb 2022 18:52:51 +0530 Subject: [PATCH 185/256] Update to layer 138 (#3698) --- telethon/_client/messages.py | 6 +++++- telethon/types/_custom/dialog.py | 5 +++++ telethon_generator/data/api.tl | 22 ++++++++++++++-------- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/telethon/_client/messages.py b/telethon/_client/messages.py index 0997a643..d0b0db75 100644 --- a/telethon/_client/messages.py +++ b/telethon/_client/messages.py @@ -673,7 +673,8 @@ async def mark_read( entity: 'hints.EntityLike', message: 'hints.MessageIDLike' = None, *, - clear_mentions: bool = False) -> bool: + clear_mentions: bool = False, + clear_reactions: bool = False) -> bool: if not message: max_id = 0 elif isinstance(message, int): @@ -685,6 +686,9 @@ async def mark_read( if clear_mentions: await self(_tl.fn.messages.ReadMentions(entity)) + if clear_reactions: + await self(_tl.fn.messages.ReadReactions(entity)) + if helpers._entity_type(entity) == helpers._EntityType.CHANNEL: return await self(_tl.fn.channels.ReadHistory( utils.get_input_channel(entity), max_id=max_id)) diff --git a/telethon/types/_custom/dialog.py b/telethon/types/_custom/dialog.py index b3b93943..5f7d8d82 100644 --- a/telethon/types/_custom/dialog.py +++ b/telethon/types/_custom/dialog.py @@ -55,6 +55,10 @@ class Dialog: How many mentions are currently unread in this dialog. Note that this value won't update when new messages arrive. + unread_reactions_count (`int`): + How many reactions are currently unread in this dialog. Note that + this value won't update when new messages arrive. + draft (`Draft `): The draft object in this dialog. It will not be `None`, so you can call ``draft.set_message(...)``. @@ -86,6 +90,7 @@ class Dialog: self.unread_count = dialog.unread_count self.unread_mentions_count = dialog.unread_mentions_count + self.unread_reactions_count = dialog.unread_reactions_count self.draft = Draft(client, self.entity, self.dialog.draft) diff --git a/telethon_generator/data/api.tl b/telethon_generator/data/api.tl index 9aaa0e57..db7d9a52 100644 --- a/telethon_generator/data/api.tl +++ b/telethon_generator/data/api.tl @@ -190,7 +190,7 @@ messageActionGroupCallScheduled#b3a07661 call:InputGroupCall schedule_date:int = messageActionSetChatTheme#aa786345 emoticon:string = MessageAction; messageActionChatJoinedByRequest#ebbca3cb = MessageAction; -dialog#2c171f72 flags:# pinned:flags.2?true unread_mark:flags.3?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int = Dialog; +dialog#a8edd0f5 flags:# pinned:flags.2?true unread_mark:flags.3?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int unread_reactions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int = Dialog; dialogFolder#71bd134c flags:# pinned:flags.2?true folder:Folder peer:Peer top_message:int unread_muted_peers_count:int unread_unmuted_peers_count:int unread_muted_messages_count:int unread_unmuted_messages_count:int = Dialog; photoEmpty#2331b22d id:long = Photo; @@ -566,7 +566,7 @@ inputStickerSetAnimatedEmoji#28703c8 = InputStickerSet; inputStickerSetDice#e67f520e emoticon:string = InputStickerSet; inputStickerSetAnimatedEmojiAnimations#cde3739 = InputStickerSet; -stickerSet#d7df217a flags:# archived:flags.1?true official:flags.2?true masks:flags.3?true animated:flags.5?true installed_date:flags.0?int id:long access_hash:long title:string short_name:string thumbs:flags.4?Vector thumb_dc_id:flags.4?int thumb_version:flags.4?int count:int hash:int = StickerSet; +stickerSet#d7df217a flags:# archived:flags.1?true official:flags.2?true masks:flags.3?true animated:flags.5?true gifs:flags.6?true installed_date:flags.0?int id:long access_hash:long title:string short_name:string thumbs:flags.4?Vector thumb_dc_id:flags.4?int thumb_version:flags.4?int count:int hash:int = StickerSet; messages.stickerSet#b60a24a6 set:StickerSet packs:Vector documents:Vector = messages.StickerSet; messages.stickerSetNotModified#d3f924eb = messages.StickerSet; @@ -1307,17 +1307,20 @@ auth.loggedOut#c3a2835f flags:# future_auth_token:flags.0?bytes = auth.LoggedOut reactionCount#6fb250d1 flags:# chosen:flags.0?true reaction:string count:int = ReactionCount; -messageReactions#87b6e36 flags:# min:flags.0?true can_see_list:flags.2?true results:Vector recent_reactons:flags.1?Vector = MessageReactions; +messageReactions#4f2b9479 flags:# min:flags.0?true can_see_list:flags.2?true results:Vector recent_reactions:flags.1?Vector = MessageReactions; -messageUserReaction#932844fa user_id:long reaction:string = MessageUserReaction; - -messages.messageReactionsList#a366923c flags:# count:int reactions:Vector users:Vector next_offset:flags.0?string = messages.MessageReactionsList; +messages.messageReactionsList#31bd492d flags:# count:int reactions:Vector chats:Vector users:Vector next_offset:flags.0?string = messages.MessageReactionsList; availableReaction#c077ec01 flags:# inactive:flags.0?true reaction:string title:string static_icon:Document appear_animation:Document select_animation:Document activate_animation:Document effect_animation:Document around_animation:flags.1?Document center_icon:flags.1?Document = AvailableReaction; messages.availableReactionsNotModified#9f071957 = messages.AvailableReactions; messages.availableReactions#768e3aad hash:int reactions:Vector = messages.AvailableReactions; +messages.translateNoResult#67ca4737 = messages.TranslatedText; +messages.translateResultText#a214f7d0 text:string = messages.TranslatedText; + +messagePeerReaction#51b67eff flags:# big:flags.0?true unread:flags.1?true peer_id:Peer reaction:string = MessagePeerReaction; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1596,12 +1599,15 @@ messages.hideChatJoinRequest#7fe7e815 flags:# approved:flags.0?true peer:InputPe messages.hideAllChatJoinRequests#e085f4ea flags:# approved:flags.0?true peer:InputPeer link:flags.1?string = Updates; messages.toggleNoForwards#b11eafa2 peer:InputPeer enabled:Bool = Updates; messages.saveDefaultSendAs#ccfddf96 peer:InputPeer send_as:InputPeer = Bool; -messages.sendReaction#25690ce4 flags:# peer:InputPeer msg_id:int reaction:flags.0?string = Updates; +messages.sendReaction#25690ce4 flags:# big:flags.1?true peer:InputPeer msg_id:int reaction:flags.0?string = Updates; messages.getMessagesReactions#8bba90e6 peer:InputPeer id:Vector = Updates; messages.getMessageReactionsList#e0ee6b77 flags:# peer:InputPeer id:int reaction:flags.0?string offset:flags.1?string limit:int = messages.MessageReactionsList; messages.setChatAvailableReactions#14050ea6 peer:InputPeer available_reactions:Vector = Updates; messages.getAvailableReactions#18dea0ac hash:int = messages.AvailableReactions; messages.setDefaultReaction#d960c4d4 reaction:string = Bool; +messages.translateText#24ce6dee flags:# peer:flags.0?InputPeer msg_id:flags.0?int text:flags.1?string from_lang:flags.2?string to_lang:string = messages.TranslatedText; +messages.getUnreadReactions#e85bae1a peer:InputPeer offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; +messages.readReactions#82e251d7 peer:InputPeer = messages.AffectedHistory; updates.getState#edd4882a = updates.State; updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference; @@ -1750,4 +1756,4 @@ stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel stats.getMessagePublicForwards#5630281b channel:InputChannel msg_id:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats; -// LAYER 137 +// LAYER 138 \ No newline at end of file From cb628f3cce8a1eb81deba80dec5963cf1712b71c Mon Sep 17 00:00:00 2001 From: Shrimadhav U K Date: Sat, 5 Feb 2022 19:24:54 +0530 Subject: [PATCH 186/256] Update to layer 138 (again) (#3699) --- telethon_generator/data/api.tl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/telethon_generator/data/api.tl b/telethon_generator/data/api.tl index db7d9a52..042b99d0 100644 --- a/telethon_generator/data/api.tl +++ b/telethon_generator/data/api.tl @@ -1704,7 +1704,7 @@ payments.getSavedInfo#227d824b = payments.SavedInfo; payments.clearSavedInfo#d83d70c1 flags:# credentials:flags.0?true info:flags.1?true = Bool; payments.getBankCardData#2e79d779 number:string = payments.BankCardData; -stickers.createStickerSet#9021ab67 flags:# masks:flags.0?true animated:flags.1?true user_id:InputUser title:string short_name:string thumb:flags.2?InputDocument stickers:Vector software:flags.3?string = messages.StickerSet; +stickers.createStickerSet#9021ab67 flags:# masks:flags.0?true animated:flags.1?true gifs:flags.4?true user_id:InputUser title:string short_name:string thumb:flags.2?InputDocument stickers:Vector software:flags.3?string = messages.StickerSet; stickers.removeStickerFromSet#f7760f51 sticker:InputDocument = messages.StickerSet; stickers.changeStickerPosition#ffb6d4ca sticker:InputDocument position:int = messages.StickerSet; stickers.addStickerToSet#8653febe stickerset:InputStickerSet sticker:InputStickerSetItem = messages.StickerSet; @@ -1756,4 +1756,4 @@ stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel stats.getMessagePublicForwards#5630281b channel:InputChannel msg_id:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats; -// LAYER 138 \ No newline at end of file +// LAYER 138 From 46ba9696aa6f6b98bb2cb0ac4745149b0d3c4cc2 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 4 Feb 2022 11:46:08 +0100 Subject: [PATCH 187/256] Fix add_handler auto-type detection --- telethon/_client/updates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index 2477af9c..e308dfab 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -56,7 +56,7 @@ def add_event_handler( if event is None: for param in inspect.signature(callback).parameters.values(): - event = param.annotation + event = None if param.annotation is inspect.Signature.empty else param.annotation break # only check the first parameter if event is None: From d490cf0f70b5a466b2e01a9cf0c165a2830c7298 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 4 Feb 2022 11:46:38 +0100 Subject: [PATCH 188/256] Add missing NotResolved import --- telethon/_client/updates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index e308dfab..d8612f6a 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -15,7 +15,7 @@ from collections import deque from ..errors._rpcbase import RpcError from .._events.raw import Raw from .._events.base import StopPropagation, EventBuilder, EventHandler -from .._events.filters import make_filter +from .._events.filters import make_filter, NotResolved from .._misc import utils from .. import _tl From ab6d60dab296905a32b60eca59f4391d843d059a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 4 Feb 2022 11:46:57 +0100 Subject: [PATCH 189/256] Fix event building --- telethon/_client/updates.py | 3 ++- telethon/_events/messageedited.py | 4 ++-- telethon/_events/newmessage.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index d8612f6a..61cbf4e7 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -216,7 +216,8 @@ async def _dispatch(self, update): for handler in self._update_handlers: event = event_cache.get(handler._event) if not event: - event_cache[handler._event] = event = handler._event._build(update) + event_cache[handler._event] = event = handler._event._build( + update, [], self._session_state.user_id, {}, self) while True: # filters can be modified at any time, and there can be any amount of them which are not yet resolved diff --git a/telethon/_events/messageedited.py b/telethon/_events/messageedited.py index 38c512ec..373f5fee 100644 --- a/telethon/_events/messageedited.py +++ b/telethon/_events/messageedited.py @@ -41,7 +41,7 @@ class MessageEdited(EventBuilder): print('Message', event.id, 'changed at', event.date) """ @classmethod - def _build(cls, update, others=None, self_id=None, *todo, **todo2): + def _build(cls, update, others, self_id, entities, client): if isinstance(update, (_tl.UpdateEditMessage, _tl.UpdateEditChannelMessage)): - return cls.Event(update.message) + return cls._new(client, update.message, entities, None) diff --git a/telethon/_events/newmessage.py b/telethon/_events/newmessage.py index ee62c655..38b3d8f7 100644 --- a/telethon/_events/newmessage.py +++ b/telethon/_events/newmessage.py @@ -109,4 +109,4 @@ class NewMessage(EventBuilder, _custom.Message): else: return - return cls.Event(_custom.Message._new(client, msg, entities, None)) + return cls._new(client, msg, entities, None) From 96b4059ae8b3a74f03d3e843107d7984865a038b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 4 Feb 2022 11:47:22 +0100 Subject: [PATCH 190/256] Properly handle exception cases during update dispatch --- telethon/_client/updates.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index 61cbf4e7..16c20266 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -225,8 +225,11 @@ async def _dispatch(self, update): if handler._filter(event): try: await handler._callback(event) + except StopPropagation: + self._dispatching_update_handlers = False + return except Exception: - name = getattr(handler.callback, '__name__', repr(handler.callback)) + name = getattr(handler._callback, '__name__', repr(handler._callback)) self._log[__name__].exception('Unhandled exception on %s (this is likely a bug in your code)', name) except NotResolved as nr: try: From 4b477e5b27c2e8747fca6958d0c1b8ead421e7f9 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 4 Feb 2022 11:52:04 +0100 Subject: [PATCH 191/256] Add some missing filters --- telethon/_events/filters/messages.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/telethon/_events/filters/messages.py b/telethon/_events/filters/messages.py index a6bacbd6..e1b1c3a3 100644 --- a/telethon/_events/filters/messages.py +++ b/telethon/_events/filters/messages.py @@ -2,6 +2,24 @@ import re from .base import Filter +class Incoming: + """ + The update must be something the client received from another user, + and not something the current user sent. + """ + def __call__(self, event): + return not event.out + + +class Outgoing: + """ + The update must be something the current user sent, + and not something received from another user. + """ + def __call__(self, event): + return event.out + + class Pattern: """ The update type must match the specified instances for the filter to return `True`. From 56faccf151716e9a7bbae1109a5f9a01ec4a588a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 4 Feb 2022 12:19:53 +0100 Subject: [PATCH 192/256] Fix and update usage of parse_mode --- telethon/_client/auth.py | 6 +--- telethon/_client/messageparse.py | 3 +- telethon/_client/telegrambaseclient.py | 1 - telethon/_client/telegramclient.py | 7 ++-- telethon/_misc/utils.py | 47 ++++++++++---------------- telethon/types/_custom/message.py | 38 +++------------------ telethon_examples/quart_login.py | 7 ++-- 7 files changed, 35 insertions(+), 74 deletions(-) diff --git a/telethon/_client/auth.py b/telethon/_client/auth.py index 7d122d8f..59f64933 100644 --- a/telethon/_client/auth.py +++ b/telethon/_client/auth.py @@ -284,11 +284,7 @@ async def sign_up( pass # code is correct and was used, now need to sign in if self._tos and self._tos.text: - if self.parse_mode: - t = self.parse_mode.unparse(self._tos.text, self._tos.entities) - else: - t = self._tos.text - sys.stderr.write("{}\n".format(t)) + sys.stderr.write("{}\n".format(self._tos.text)) sys.stderr.flush() phone, phone_code_hash = \ diff --git a/telethon/_client/messageparse.py b/telethon/_client/messageparse.py index f545a2f3..1366bca3 100644 --- a/telethon/_client/messageparse.py +++ b/telethon/_client/messageparse.py @@ -4,6 +4,7 @@ import typing from .._misc import helpers, utils from ..types import _custom +from ..types._custom.inputmessage import InputMessage from .. import _tl if typing.TYPE_CHECKING: @@ -29,7 +30,7 @@ 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 + parse_mode = InputMessage._default_parse_mode else: parse_mode = utils.sanitize_parse_mode(parse_mode) diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 8af7ec52..948c6776 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -141,7 +141,6 @@ def init( self._connect_timeout = connect_timeout self.flood_sleep_threshold = flood_sleep_threshold self._flood_waited_requests = {} # prevent calls that would floodwait entirely - self._parse_mode = markdown # Update handling. self._catch_up = catch_up diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 851f39d1..0cd55d4a 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -2196,7 +2196,8 @@ class TelegramClient: await client.send_message('me', 'Hello **world**!') # Default to another parse mode - client.parse_mode = 'html' + from telethon.types import Message + Message.set_default_parse_mode('html') await client.send_message('me', 'Some bold and italic text') await client.send_message('me', 'An URL') @@ -2204,8 +2205,8 @@ class TelegramClient: await client.send_message('me', 'Mentions') # Explicit parse mode - # No parse mode by default - client.parse_mode = None + # No parse mode by default (import Message first) + Message.set_default_parse_mode(None) # ...but here I want markdown await client.send_message('me', 'Hello, **world**!', parse_mode='md') diff --git a/telethon/_misc/utils.py b/telethon/_misc/utils.py index b8c62b6a..b2834b93 100644 --- a/telethon/_misc/utils.py +++ b/telethon/_misc/utils.py @@ -748,37 +748,26 @@ def get_attributes(file, *, attributes=None, mime_type=None, return list(attr_dict.values()), mime_type -def sanitize_parse_mode(mode): - """ - Converts the given parse mode into an object with - ``parse`` and ``unparse`` callable properties. - """ - if not mode: - return None - - if callable(mode): - class CustomMode: - @staticmethod - def unparse(text, entities): - raise NotImplementedError - - CustomMode.parse = mode - return CustomMode - elif (all(hasattr(mode, x) for x in ('parse', 'unparse')) - and all(callable(x) for x in (mode.parse, mode.unparse))): - return mode +def sanitize_parse_mode(mode, *, _nop_parse=lambda t: (t, []), _nop_unparse=lambda t, e: t): + if mode is None: + mode = (_nop_parse, _nop_unparse) elif isinstance(mode, str): - try: - return { - 'md': markdown, - 'markdown': markdown, - 'htm': html, - 'html': html - }[mode.lower()] - except KeyError: - raise ValueError('Unknown parse mode {}'.format(mode)) + mode = mode.lower() + if mode in ('md', 'markdown'): + mode = (markdown.parse, markdown.unparse) + elif mode in ('htm', 'html'): + mode = (html.parse, html.unparse) + else: + raise ValueError(f'mode must be one of md, markdown, htm or html, but was {mode!r}') + elif callable(mode): + mode = (mode, _nop_unparse) + elif isinstance(mode, tuple): + if not (len(mode) == 2 and callable(mode[0]) and callable(mode[1])): + raise ValueError(f'mode must be a tuple of exactly two callables') else: - raise TypeError('Invalid parse mode type {}'.format(mode)) + raise TypeError(f'mode must be either a str, callable or tuple, but was {mode!r}') + + return mode def get_input_location(location): diff --git a/telethon/types/_custom/message.py b/telethon/types/_custom/message.py index e1bfff41..17f70a90 100644 --- a/telethon/types/_custom/message.py +++ b/telethon/types/_custom/message.py @@ -489,7 +489,8 @@ class Message(ChatGetter, SenderGetter): * A string equal to ``'md'`` or ``'markdown`` for parsing with commonmark, ``'htm'`` or ``'html'`` for parsing HTML. * A ``callable``, which accepts a ``str`` as input and returns a tuple of - ``(parsed str, formatting entities)``. + ``(parsed str, formatting entities)``. Obtaining formatted text from a message in + this setting is not supported and will instead return the plain text. * A ``tuple`` of two ``callable``. The first must accept a ``str`` as input and return a tuple of ``(parsed str, list of formatting entities)``. The second must accept two parameters, a parsed ``str`` and a ``list`` of formatting entities, and must return @@ -497,25 +498,7 @@ class Message(ChatGetter, SenderGetter): If it's not one of these values or types, the method fails accordingly. """ - if isinstance(mode, str): - mode = mode.lower() - if mode in ('md', 'markdown'): - mode = (_misc.markdown.parse, _misc.markdown.unparse) - elif mode in ('htm', 'html'): - mode = (_misc.html.parse, _misc.html.unparse) - else: - raise ValueError(f'mode must be one of md, markdown, htm or html, but was {mode!r}') - elif callable(mode): - mode = (mode, lambda t, e: t) - elif isinstance(mode, tuple): - if len(mode) == 2 and callable(mode[0]) and callable(mode[1]): - mode = mode - else: - raise ValueError(f'mode must be a tuple of exactly two callables') - else: - raise TypeError(f'mode must be either a str, callable or tuple, but was {mode!r}') - - InputMessage._default_parse_mode = mode + InputMessage._default_parse_mode = utils.sanitize_parse_mode(mode) @classmethod def set_default_link_preview(cls, enabled): @@ -545,22 +528,11 @@ class Message(ChatGetter, SenderGetter): The message text, formatted using the client's default parse mode. Will be `None` for :tl:`MessageService`. """ - if self._text is None and self._client: - if not self._client.parse_mode: - self._text = self.message - else: - self._text = self._client.parse_mode.unparse( - self.message, self.entities) - - return self._text + return InputMessage._default_parse_mode[1](self.message, self.entities) @text.setter def text(self, value): - self._text = value - if self._client and self._client.parse_mode: - self.message, self.entities = self._client.parse_mode.parse(value) - else: - self.message, self.entities = value, [] + self.message, self.entities = InputMessage._default_parse_mode[0](value) @property def raw_text(self): diff --git a/telethon_examples/quart_login.py b/telethon_examples/quart_login.py index 20eae383..507c55a5 100644 --- a/telethon_examples/quart_login.py +++ b/telethon_examples/quart_login.py @@ -5,6 +5,7 @@ import hypercorn.asyncio from quart import Quart, render_template_string, request from telethon import TelegramClient, utils +from telethon.types import Message from telethon.errors import SessionPasswordNeededError @@ -51,9 +52,11 @@ SESSION = os.environ.get('TG_SESSION', 'quart') API_ID = int(get_env('TG_API_ID', 'Enter your API ID: ')) API_HASH = get_env('TG_API_HASH', 'Enter your API hash: ') +# Render things nicely (global setting) +Message.set_default_parse_mode('html') + # Telethon client client = TelegramClient(SESSION, API_ID, API_HASH) -client.parse_mode = 'html' # <- Render things nicely phone = None # Quart app @@ -69,7 +72,7 @@ async def format_message(message): message.raw_text ) else: - # client.parse_mode = 'html', so bold etc. will work! + # The Message parse_mode is 'html', so bold etc. will work! content = (message.text or '(action message)').replace('\n', '
') return '

{}: {}{}

'.format( From 3b12cc3e51b2f2a0822b79850fac4f6d85658059 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 4 Feb 2022 13:05:27 +0100 Subject: [PATCH 193/256] Fix stringify for custom Message --- telethon/_misc/helpers.py | 2 +- telethon/types/_custom/message.py | 36 ------------------------------- 2 files changed, 1 insertion(+), 37 deletions(-) diff --git a/telethon/_misc/helpers.py b/telethon/_misc/helpers.py index c6c76b8e..64dabd28 100644 --- a/telethon/_misc/helpers.py +++ b/telethon/_misc/helpers.py @@ -194,7 +194,7 @@ def pretty_print(obj, indent=None, max_depth=float('inf')): if max_depth < 0: return '...' - to_d = getattr(obj, '_to_dict', None) or getattr(obj, 'to_dict', None) + to_d = getattr(obj, 'to_dict', None) if callable(to_d): obj = to_d() diff --git a/telethon/types/_custom/message.py b/telethon/types/_custom/message.py index 17f70a90..42d4aeab 100644 --- a/telethon/types/_custom/message.py +++ b/telethon/types/_custom/message.py @@ -1334,39 +1334,6 @@ class Message(ChatGetter, SenderGetter): def to_dict(self): return self._message.to_dict() - def _to_dict(self): - return { - '_': 'Message', - 'id': self.id, - 'out': self.out, - 'date': self.date, - 'text': self.text, - 'sender': self.sender, - 'chat': self.chat, - 'mentioned': self.mentioned, - 'media_unread': self.media_unread, - 'silent': self.silent, - 'post': self.post, - 'from_scheduled': self.from_scheduled, - 'legacy': self.legacy, - 'edit_hide': self.edit_hide, - 'pinned': self.pinned, - 'forward': self.forward, - 'via_bot': self.via_bot, - 'reply_to': self.reply_to, - 'reply_markup': self.reply_markup, - 'views': self.views, - 'forwards': self.forwards, - 'replies': self.replies, - 'edit_date': self.edit_date, - 'post_author': self.post_author, - 'grouped_id': self.grouped_id, - 'ttl_period': self.ttl_period, - 'action': self.action, - 'media': self.media, - 'action_entities': self.action_entities, - } - def __repr__(self): return helpers.pretty_print(self) @@ -1375,6 +1342,3 @@ class Message(ChatGetter, SenderGetter): def stringify(self): return helpers.pretty_print(self, indent=0) - - -# TODO set md by default if commonmark is installed else nothing From ae43a47602d501ba10b2a3f9735d4c529593cab8 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 4 Feb 2022 13:09:57 +0100 Subject: [PATCH 194/256] Update iter_ usages with get_ --- readthedocs/basic/quick-start.rst | 4 +- readthedocs/concepts/asyncio.rst | 2 +- readthedocs/concepts/updates.rst | 2 +- .../quick-references/objects-reference.rst | 8 ++-- telethon/_client/messages.py | 2 +- telethon/_client/telegramclient.py | 38 +++++++++---------- telethon/types/_custom/chatgetter.py | 2 +- telethon_examples/quart_login.py | 2 +- 8 files changed, 30 insertions(+), 30 deletions(-) diff --git a/readthedocs/basic/quick-start.rst b/readthedocs/basic/quick-start.rst index bd36b048..8a100db9 100644 --- a/readthedocs/basic/quick-start.rst +++ b/readthedocs/basic/quick-start.rst @@ -32,7 +32,7 @@ use these if possible. print(me.phone) # You can print all the dialogs/conversations that you are part of: - async for dialog in client.iter_dialogs(): + async for dialog in client.get_dialogs(): print(dialog.name, 'has ID', dialog.id) # You can send messages to yourself... @@ -62,7 +62,7 @@ use these if possible. await client.send_file('me', '/home/me/Pictures/holidays.jpg') # You can print the message history of any chat: - async for message in client.iter_messages('me'): + async for message in client.get_messages('me'): print(message.id, message.text) # You can download media from messages, too! diff --git a/readthedocs/concepts/asyncio.rst b/readthedocs/concepts/asyncio.rst index dd85f957..00781be7 100644 --- a/readthedocs/concepts/asyncio.rst +++ b/readthedocs/concepts/asyncio.rst @@ -75,7 +75,7 @@ loops or use ``async with``: async with client: # ^ this is an asynchronous with block - async for message in client.iter_messages(chat): + async for message in client.get_messages(chat): # ^ this is a for loop over an asynchronous generator print(message.sender.username) diff --git a/readthedocs/concepts/updates.rst b/readthedocs/concepts/updates.rst index 0c80c344..ce6cc1b0 100644 --- a/readthedocs/concepts/updates.rst +++ b/readthedocs/concepts/updates.rst @@ -28,7 +28,7 @@ In short, you should do this: buttons = await event.get_buttons() async def main(): - async for message in client.iter_messages('me', 10): + async for message in client.get_messages('me', 10): # Methods from the client always have these properties ready chat = message.input_chat sender = message.sender diff --git a/readthedocs/quick-references/objects-reference.rst b/readthedocs/quick-references/objects-reference.rst index 41f73033..f1d7df4f 100644 --- a/readthedocs/quick-references/objects-reference.rst +++ b/readthedocs/quick-references/objects-reference.rst @@ -159,8 +159,8 @@ AdminLogEvent ============= The `AdminLogEvent ` object -is returned by the `client.iter_admin_log() -` method to easily iterate +is returned by the `client.get_admin_log() +` method to easily iterate over past "events" (deleted messages, edits, title changes, leaving members…) These are all the properties you can find in it: @@ -270,7 +270,7 @@ Dialog ====== The `Dialog ` object is returned when -you call `client.iter_dialogs() `. +you call `client.get_dialogs() `. .. currentmodule:: telethon.tl.custom.dialog.Dialog @@ -286,7 +286,7 @@ Draft ====== The `Draft ` object is returned when -you call `client.iter_drafts() `. +you call `client.get_drafts() `. .. currentmodule:: telethon.tl.custom.draft.Draft diff --git a/telethon/_client/messages.py b/telethon/_client/messages.py index d0b0db75..1d85a5ee 100644 --- a/telethon/_client/messages.py +++ b/telethon/_client/messages.py @@ -140,7 +140,7 @@ class _MessagesIter(requestiter.RequestIter): # trigger `RPC_CALL_FAIL` which is "internal issues"... if not isinstance(filter, _tl.InputMessagesFilterEmpty) \ and offset_date and not search and not offset_id: - async for m in self.client.iter_messages( + async for m in self.client.get_messages( self.entity, 1, offset_date=offset_date): self.request.offset_id = m.id + 1 else: diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 0cd55d4a..6a6cc1d9 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -190,7 +190,7 @@ class TelegramClient: .. code-block:: python async with client.takeout(): - async for message in client.iter_messages(chat, wait_time=0): + async for message in client.get_messages(chat, wait_time=0): ... # Do something with the message """ @@ -214,8 +214,8 @@ class TelegramClient: 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 - `. + to adjust the `wait_time` of methods like `client.get_messages + `. By default, all parameters are `None`, and you need to enable those you plan to use by setting them to either `True` or `False`. @@ -272,7 +272,7 @@ class TelegramClient: await client.get_messages('me') # wrapped through takeout (less limits) - async for message in client.iter_messages(chat, wait_time=0): + async for message in client.get_messages(chat, wait_time=0): ... # Do something with the message await client.end_takeout(success=True) @@ -751,16 +751,16 @@ class TelegramClient: .. code-block:: python # Show all user IDs in a chat - async for user in client.iter_participants(chat): + async for user in client.get_participants(chat): print(user.id) # Search by name - async for user in client.iter_participants(chat, search='name'): + async for user in client.get_participants(chat, search='name'): print(user.username) # Filter by admins from telethon.tl.types import ChannelParticipantsAdmins - async for user in client.iter_participants(chat, filter=ChannelParticipantsAdmins): + async for user in client.get_participants(chat, filter=ChannelParticipantsAdmins): print(user.first_name) # Get a list of 0 people but print the total amount of participants in the chat @@ -892,7 +892,7 @@ class TelegramClient: Example .. code-block:: python - async for event in client.iter_admin_log(channel): + async for event in client.get_admin_log(channel): if event.changed_title: print('The title changed from', event.old, 'to', event.new) @@ -943,7 +943,7 @@ class TelegramClient: .. code-block:: python # Download all the profile photos of some user - async for photo in client.iter_profile_photos(user): + async for photo in client.get_profile_photos(user): await client.download_media(photo) # Get all the photos of a channel and download the oldest one @@ -1456,7 +1456,7 @@ class TelegramClient: .. code-block:: python # Print all dialog IDs and the title, nicely formatted - async for dialog in client.iter_dialogs(): + async for dialog in client.get_dialogs(): print('{:>14}: {}'.format(dialog.id, dialog.title)) # Get all open conversation, print the title of the first @@ -1502,7 +1502,7 @@ class TelegramClient: await draft.delete() # Getting the drafts with 'bot1' and 'bot2' - async for draft in client.iter_drafts(['bot1', 'bot2']): + async for draft in client.get_drafts(['bot1', 'bot2']): print(draft.text) # Get the draft in your chat @@ -1998,28 +1998,28 @@ class TelegramClient: .. code-block:: python # From most-recent to oldest - async for message in client.iter_messages(chat): + async for message in client.get_messages(chat): print(message.id, message.text) # From oldest to most-recent - async for message in client.iter_messages(chat, reverse=True): + async for message in client.get_messages(chat, reverse=True): print(message.id, message.text) # Filter by sender, and limit to 10 - async for message in client.iter_messages(chat, 10, from_user='me'): + async for message in client.get_messages(chat, 10, from_user='me'): print(message.text) # Server-side search with fuzzy text - async for message in client.iter_messages(chat, search='hello'): + async for message in client.get_messages(chat, search='hello'): print(message.id) # Filter by message type: from telethon.tl.types import InputMessagesFilterPhotos - async for message in client.iter_messages(chat, filter=InputMessagesFilterPhotos): + async for message in client.get_messages(chat, filter=InputMessagesFilterPhotos): print(message.photo) # Getting comments from a post in a channel: - async for message in client.iter_messages(channel, reply_to=123): + async for message in client.get_messages(channel, reply_to=123): print(message.chat.title, message.text) # Get 0 photos and print the total to show how many photos there are @@ -3337,12 +3337,12 @@ class TelegramClient: print(utils.get_display_name(me)) chat = await client.get_input_entity('username') - async for message in client.iter_messages(chat): + async for message in client.get_messages(chat): ... # Note that you could have used the username directly, but it's # good to use get_input_entity if you will reuse it a lot. - async for message in client.iter_messages('username'): + async for message in client.get_messages('username'): ... # Note that for this to work the phone number must be in your contacts diff --git a/telethon/types/_custom/chatgetter.py b/telethon/types/_custom/chatgetter.py index 6bd4c1c3..c92ffb00 100644 --- a/telethon/types/_custom/chatgetter.py +++ b/telethon/types/_custom/chatgetter.py @@ -75,7 +75,7 @@ class ChatGetter(abc.ABC): try: # The chat may be recent, look in dialogs target = self.chat_id - async for d in self._client.iter_dialogs(100): + async for d in self._client.get_dialogs(100): if d.id == target: self._chat = d.entity self._input_chat = d.input_entity diff --git a/telethon_examples/quart_login.py b/telethon_examples/quart_login.py index 507c55a5..5f5f1b00 100644 --- a/telethon_examples/quart_login.py +++ b/telethon_examples/quart_login.py @@ -119,7 +119,7 @@ async def root(): # They are logged in, show them some messages from their first dialog dialog = (await client.get_dialogs())[0] result = '

{}

'.format(dialog.title) - async for m in client.iter_messages(dialog, 10): + async for m in client.get_messages(dialog, 10): result += await(format_message(m)) return await render_template_string(BASE_TEMPLATE, content=result) From d87b68a75621dc3be06a0933e9b5e8d73a43d460 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 7 Feb 2022 09:28:39 +0100 Subject: [PATCH 195/256] Fix direct mutation of objects in friendly methods --- telethon/_client/chats.py | 56 +++++++++++++++----------------- telethon/_client/dialogs.py | 14 +++++--- telethon/_client/downloads.py | 15 +++++---- telethon/_client/messageparse.py | 14 ++++---- telethon/_client/messages.py | 21 ++++++------ telethon/_client/users.py | 6 ++-- 6 files changed, 65 insertions(+), 61 deletions(-) diff --git a/telethon/_client/chats.py b/telethon/_client/chats.py index e8a95291..a9a4a45f 100644 --- a/telethon/_client/chats.py +++ b/telethon/_client/chats.py @@ -3,6 +3,7 @@ import inspect import itertools import string import typing +import dataclasses from .. import errors, _tl from .._misc import helpers, utils, requestiter, tlobject, enums, hints @@ -19,11 +20,9 @@ _MAX_PROFILE_PHOTO_CHUNK_SIZE = 100 class _ChatAction: def __init__(self, client, chat, action, *, delay, auto_cancel): self._client = client - self._chat = chat - self._action = action self._delay = delay self._auto_cancel = auto_cancel - self._request = None + self._request = _tl.fn.messages.SetTyping(chat, action) self._task = None self._running = False @@ -31,14 +30,7 @@ class _ChatAction: return self._once().__await__() async def __aenter__(self): - self._chat = await self._client.get_input_entity(self._chat) - - # Since `self._action` is passed by reference we can avoid - # recreating the request all the time and still modify - # `self._action.progress` directly in `progress`. - self._request = _tl.fn.messages.SetTyping( - self._chat, self._action) - + self._request = dataclasses.replace(self._request, peer=await self._client.get_input_entity(self._request.peer)) self._running = True self._task = asyncio.create_task(self._update()) return self @@ -55,7 +47,7 @@ class _ChatAction: self._task = None async def _once(self): - self._chat = await self._client.get_input_entity(self._chat) + self._request = dataclasses.replace(self._request, peer=await self._client.get_input_entity(self._request.peer)) await self._client(_tl.fn.messages.SetTyping(self._chat, self._action)) async def _update(self): @@ -93,8 +85,11 @@ class _ChatAction: }[enums.Action(action)] def progress(self, current, total): - if hasattr(self._action, 'progress'): - self._action.progress = 100 * round(current / total) + if hasattr(self._request.action, 'progress'): + self._request = dataclasses.replace( + self._request, + action=dataclasses.replace(self._request.action, progress=100 * round(current / total)) + ) class _ParticipantsIter(requestiter.RequestIter): @@ -190,8 +185,8 @@ class _ParticipantsIter(requestiter.RequestIter): # Most people won't care about getting exactly 12,345 # members so it doesn't really matter not to be 100% # precise with being out of the offset/limit here. - self.request.limit = min( - self.limit - self.request.offset, _MAX_PARTICIPANTS_CHUNK_SIZE) + self.request = dataclasses.replace(self.request, limit=min( + self.limit - self.request.offset, _MAX_PARTICIPANTS_CHUNK_SIZE)) if self.request.offset > self.limit: return True @@ -199,7 +194,7 @@ class _ParticipantsIter(requestiter.RequestIter): participants = await self.client(self.request) self.total = participants.count - self.request.offset += len(participants.participants) + self.request = dataclasses.replace(self.request, offset=self.request.offset + len(participants.participants)) users = {user.id: user for user in participants.users} for participant in participants.participants: if isinstance(participant, _tl.ChannelParticipantBanned): @@ -253,20 +248,20 @@ class _AdminLogIter(requestiter.RequestIter): ) async def _load_next_chunk(self): - self.request.limit = min(self.left, _MAX_ADMIN_LOG_CHUNK_SIZE) + self.request = dataclasses.replace(self.request, limit=min(self.left, _MAX_ADMIN_LOG_CHUNK_SIZE)) r = await self.client(self.request) entities = {utils.get_peer_id(x): x for x in itertools.chain(r.users, r.chats)} - self.request.max_id = min((e.id for e in r.events), default=0) + self.request = dataclasses.replace(self.request, max_id=min((e.id for e in r.events), default=0)) for ev in r.events: if isinstance(ev.action, _tl.ChannelAdminLogEventActionEditMessage): - ev.action.prev_message = _custom.Message._new( - self.client, ev.action.prev_message, entities, self.entity) - - ev.action.new_message = _custom.Message._new( - self.client, ev.action.new_message, entities, self.entity) + ev = dataclasses.replace(ev, action=dataclasses.replace( + ev.action, + prev_message=_custom.Message._new(self.client, ev.action.prev_message, entities, self.entity), + new_message=_custom.Message._new(self.client, ev.action.new_message, entities, self.entity) + )) elif isinstance(ev.action, _tl.ChannelAdminLogEventActionDeleteMessage): @@ -308,7 +303,7 @@ class _ProfilePhotoIter(requestiter.RequestIter): ) if self.limit == 0: - self.request.limit = 1 + self.request = dataclasses.replace(self.request, limit=1) result = await self.client(self.request) if isinstance(result, _tl.photos.Photos): self.total = len(result.photos) @@ -319,7 +314,7 @@ class _ProfilePhotoIter(requestiter.RequestIter): self.total = getattr(result, 'count', None) async def _load_next_chunk(self): - self.request.limit = min(self.left, _MAX_PROFILE_PHOTO_CHUNK_SIZE) + self.request = dataclasses.replace(self.request, limit=min(self.left, _MAX_PROFILE_PHOTO_CHUNK_SIZE)) result = await self.client(self.request) if isinstance(result, _tl.photos.Photos): @@ -338,7 +333,7 @@ class _ProfilePhotoIter(requestiter.RequestIter): if len(self.buffer) < self.request.limit: self.left = len(self.buffer) else: - self.request.offset += len(result.photos) + self.request = dataclasses.replace(self.request, offset=self.request.offset + len(result.photos)) else: # Some broadcast channels have a photo that this request doesn't # retrieve for whatever random reason the Telegram server feels. @@ -368,8 +363,11 @@ class _ProfilePhotoIter(requestiter.RequestIter): if len(result.messages) < self.request.limit: self.left = len(self.buffer) elif result.messages: - self.request.add_offset = 0 - self.request.offset_id = result.messages[-1].id + self.request = dataclasses.replace( + self.request, + add_offset=0, + offset_id=result.messages[-1].id + ) def get_participants( diff --git a/telethon/_client/dialogs.py b/telethon/_client/dialogs.py index e3832ee8..e0f32547 100644 --- a/telethon/_client/dialogs.py +++ b/telethon/_client/dialogs.py @@ -2,6 +2,7 @@ import asyncio import inspect import itertools import typing +import dataclasses from .. import errors, _tl from .._misc import helpers, utils, requestiter, hints @@ -49,7 +50,7 @@ class _DialogsIter(requestiter.RequestIter): self.ignore_migrated = ignore_migrated async def _load_next_chunk(self): - self.request.limit = min(self.left, _MAX_CHUNK_SIZE) + self.request = dataclasses.replace(self.request, limit=min(self.left, _MAX_CHUNK_SIZE)) r = await self.client(self.request) self.total = getattr(r, 'count', len(r.dialogs)) @@ -103,10 +104,13 @@ class _DialogsIter(requestiter.RequestIter): for d in reversed(r.dialogs) )), None) - self.request.exclude_pinned = True - self.request.offset_id = last_message.id if last_message else 0 - self.request.offset_date = last_message.date if last_message else None - self.request.offset_peer = self.buffer[-1].input_entity + self.request = dataclasses.replace( + self.request, + exclude_pinned=True, + offset_id=last_message.id if last_message else 0, + offset_date=last_message.date if last_message else None, + offset_peer=self.buffer[-1].input_entity, + ) class _DraftsIter(requestiter.RequestIter): diff --git a/telethon/_client/downloads.py b/telethon/_client/downloads.py index 5bf8f01f..67c4622d 100644 --- a/telethon/_client/downloads.py +++ b/telethon/_client/downloads.py @@ -5,6 +5,7 @@ import pathlib import typing import inspect import asyncio +import dataclasses from .._crypto import AES from .._misc import utils, helpers, requestiter, tlobject, hints, enums @@ -55,7 +56,7 @@ class _DirectDownloadIter(requestiter.RequestIter): self.left = len(self.buffer) await self.close() else: - self.request.offset += self._stride + self.request = dataclasses.replace(self.request, offset=self.request.offset + self._stride) async def _request(self): try: @@ -102,7 +103,7 @@ class _DirectDownloadIter(requestiter.RequestIter): if document.id != self.request.location.id: raise - self.request.location.file_reference = document.file_reference + self.request.location = dataclasses.replace(self.request.location, file_reference=document.file_reference) return await self._request() async def close(self): @@ -134,18 +135,18 @@ class _GenericDownloadIter(_DirectDownloadIter): before = self.request.offset # 1.2. We have to fetch from a valid offset, so remove that bad part - self.request.offset -= bad + self.request = dataclasses.replace(self.request, offset=self.request.offset - bad) done = False while not done and len(data) - bad < self._chunk_size: cur = await self._request() - self.request.offset += self.request.limit + self.request = dataclasses.replace(self.request, offset=self.request.offset - self.request.limit) data += cur done = len(cur) < self.request.limit # 1.3 Restore our last desired offset - self.request.offset = before + self.request = dataclasses.replace(self.request, offset=before) # 2. Fill the buffer with the data we have # 2.1. Slicing `bytes` is expensive, yield `memoryview` instead @@ -157,7 +158,7 @@ class _GenericDownloadIter(_DirectDownloadIter): self.buffer.append(mem[i:i + self._chunk_size]) # 2.3. We will yield this offset, so move to the next one - self.request.offset += self._stride + self.request = dataclasses.replace(self.request, offset=self.request.offset + self._stride) # 2.4. If we are in the last chunk, we will return the last partial data if done: @@ -172,7 +173,7 @@ class _GenericDownloadIter(_DirectDownloadIter): # 3. Be careful with the offsets. Re-fetching a bit of data # is fine, since it greatly simplifies things. # TODO Try to not re-fetch data - self.request.offset -= self._stride + self.request = dataclasses.replace(self.request, offset=self.request.offset - self._stride) async def download_profile_photo( diff --git a/telethon/_client/messageparse.py b/telethon/_client/messageparse.py index 1366bca3..ab828209 100644 --- a/telethon/_client/messageparse.py +++ b/telethon/_client/messageparse.py @@ -87,7 +87,7 @@ def _get_response_message(self: 'TelegramClient', request, result, input_chat): elif isinstance(update, ( _tl.UpdateNewChannelMessage, _tl.UpdateNewMessage)): - update.message = _custom.Message._new(self, update.message, entities, input_chat) + message = _custom.Message._new(self, update.message, entities, input_chat) # Pinning a message with `updatePinnedMessage` seems to # always produce a service message we can't map so return @@ -97,20 +97,20 @@ def _get_response_message(self: 'TelegramClient', request, result, input_chat): # # 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 + id_to_message[message.id] = message else: - return update.message + return message elif (isinstance(update, _tl.UpdateEditMessage) and helpers._entity_type(request.peer) != helpers._EntityType.CHANNEL): - update.message = _custom.Message._new(self, update.message, entities, input_chat) + message = _custom.Message._new(self, update.message, 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 + id_to_message[message.id] = message + elif request.id == message.id: + return message elif (isinstance(update, _tl.UpdateEditChannelMessage) and utils.get_peer_id(request.peer) == diff --git a/telethon/_client/messages.py b/telethon/_client/messages.py index 1d85a5ee..e2065c7b 100644 --- a/telethon/_client/messages.py +++ b/telethon/_client/messages.py @@ -3,6 +3,7 @@ import itertools import time import typing import warnings +import dataclasses from .._misc import helpers, utils, requestiter, hints from ..types import _custom @@ -142,7 +143,7 @@ class _MessagesIter(requestiter.RequestIter): and offset_date and not search and not offset_id: async for m in self.client.get_messages( self.entity, 1, offset_date=offset_date): - self.request.offset_id = m.id + 1 + self.request = dataclasses.replace(self.request, offset_id=m.id + 1) else: self.request = _tl.fn.messages.GetHistory( peer=self.entity, @@ -178,10 +179,10 @@ class _MessagesIter(requestiter.RequestIter): self.last_id = 0 if self.reverse else float('inf') async def _load_next_chunk(self): - self.request.limit = min(self.left, _MAX_CHUNK_SIZE) + self.request = dataclasses.replace(self.request, limit=min(self.left, _MAX_CHUNK_SIZE)) if self.reverse and self.request.limit != _MAX_CHUNK_SIZE: # Remember that we need -limit when going in reverse - self.request.add_offset = self.add_offset - self.request.limit + self.request = dataclasses.replace(self.request, add_offset=self.add_offset - self.request.limit) r = await self.client(self.request) self.total = getattr(r, 'count', len(r.messages)) @@ -241,28 +242,28 @@ class _MessagesIter(requestiter.RequestIter): """ After making the request, update its offset with the last message. """ - self.request.offset_id = last_message.id + self.request = dataclasses.replace(self.request, offset_id=last_message.id) if self.reverse: # We want to skip the one we already have - self.request.offset_id += 1 + self.request = dataclasses.replace(self.request, offset_id=self.request.offset_id + 1) if isinstance(self.request, _tl.fn.messages.Search): # Unlike getHistory and searchGlobal that use *offset* date, # this is *max* date. This means that doing a search in reverse # will break it. Since it's not really needed once we're going # (only for the first request), it's safe to just clear it off. - self.request.max_date = None + self.request = dataclasses.replace(self.request, max_date=None) else: # getHistory, searchGlobal and getReplies call it offset_date - self.request.offset_date = last_message.date + self.request = dataclasses.replace(self.request, offset_date=last_message.date) if isinstance(self.request, _tl.fn.messages.SearchGlobal): if last_message.input_chat: - self.request.offset_peer = last_message.input_chat + self.request = dataclasses.replace(self.request, offset_peer=last_message.input_chat) else: - self.request.offset_peer = _tl.InputPeerEmpty() + self.request = dataclasses.replace(self.request, offset_peer=_tl.InputPeerEmpty()) - self.request.offset_rate = getattr(response, 'next_rate', 0) + self.request = dataclasses.replace(self.request, offset_rate=getattr(response, 'next_rate', 0)) class _IDsIter(requestiter.RequestIter): diff --git a/telethon/_client/users.py b/telethon/_client/users.py index 763a31c4..b44564e1 100644 --- a/telethon/_client/users.py +++ b/telethon/_client/users.py @@ -3,6 +3,7 @@ import datetime import itertools import time import typing +import dataclasses from ..errors._custom import MultiError from ..errors._rpcbase import RpcError, ServerError, FloodError, InvalidDcError, UnauthorizedError @@ -370,8 +371,7 @@ async def _get_input_dialog(self: 'TelegramClient', dialog): """ try: if dialog.SUBCLASS_OF_ID == 0xa21c9795: # crc32(b'InputDialogPeer') - dialog.peer = await self.get_input_entity(dialog.peer) - return dialog + return dataclasses.replace(dialog, peer=await self.get_input_entity(dialog.peer)) elif dialog.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer') return _tl.InputDialogPeer(dialog) except AttributeError: @@ -388,7 +388,7 @@ async def _get_input_notify(self: 'TelegramClient', notify): try: if notify.SUBCLASS_OF_ID == 0x58981615: if isinstance(notify, _tl.InputNotifyPeer): - notify.peer = await self.get_input_entity(notify.peer) + return dataclasses.replace(notify, peer=await self.get_input_entity(notify.peer)) return notify except AttributeError: pass From a6f53baabaee98b200ecdf81227d39733b0f694f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 7 Feb 2022 09:36:31 +0100 Subject: [PATCH 196/256] Remove client check in custom Message --- telethon/types/_custom/message.py | 80 ++++++++++++++----------------- 1 file changed, 37 insertions(+), 43 deletions(-) diff --git a/telethon/types/_custom/message.py b/telethon/types/_custom/message.py index 42d4aeab..24eed18f 100644 --- a/telethon/types/_custom/message.py +++ b/telethon/types/_custom/message.py @@ -27,6 +27,11 @@ def _fwd(field, doc): return property(fget, fset, None, doc) +class _UninitClient: + def __getattribute__(self, attr): + raise ValueError('this Message instance does not come from a chat and cannot be used') + + # TODO Figure out a way to have the code generator error on missing fields # Maybe parsing the init function alone if that's possible. class Message(ChatGetter, SenderGetter): @@ -44,6 +49,9 @@ class Message(ChatGetter, SenderGetter): file with a certain performer, duration and thumbnail. However, most properties and methods won't work (since messages you create have not yet been sent). + + Manually-created instances of this message cannot be responded to, edited, + and so on, because the message needs to first be sent for those to make sense. """ # region Forwarded properties @@ -409,6 +417,7 @@ class Message(ChatGetter, SenderGetter): buttons=buttons, ttl=ttl, ) + self._client = _UninitClient() @classmethod def _new(cls, client, message, entities, input_chat): @@ -515,10 +524,9 @@ class Message(ChatGetter, SenderGetter): def client(self): """ Returns the `TelegramClient ` - that *patched* this message. This will only be present if you - **use the friendly methods**, it won't be there if you invoke - raw API methods manually, in which case you should only access - members, not properties. + which returned this message from a friendly method. It won't be there if you + invoke raw API methods manually (because those return the original :tl:`Message`, + not this class). """ return self._client @@ -840,7 +848,7 @@ class Message(ChatGetter, SenderGetter): """ # If the client wasn't set we can't emulate the behaviour correctly, # so as a best-effort simply return the chat peer. - if self._client and not self.out and self.is_private: + if not self.out and self.is_private: return _tl.PeerUser(self._client._session_state.user_id) return self.peer_id @@ -895,7 +903,7 @@ class Message(ChatGetter, SenderGetter): The result will be cached after its first use. """ - if self._reply_message is None and self._client: + if self._reply_message is None: if not self.reply_to: return None @@ -923,9 +931,8 @@ class Message(ChatGetter, SenderGetter): `telethon.client.messages.MessageMethods.send_message` with ``entity`` already set. """ - if self._client: - return await self._client.send_message( - await self.get_input_chat(), *args, **kwargs) + return await self._client.send_message( + await self.get_input_chat(), *args, **kwargs) async def reply(self, *args, **kwargs): """ @@ -933,10 +940,9 @@ class Message(ChatGetter, SenderGetter): `telethon.client.messages.MessageMethods.send_message` with both ``entity`` and ``reply_to`` already set. """ - if self._client: - kwargs['reply_to'] = self.id - return await self._client.send_message( - await self.get_input_chat(), *args, **kwargs) + kwargs['reply_to'] = self.id + return await self._client.send_message( + await self.get_input_chat(), *args, **kwargs) async def forward_to(self, *args, **kwargs): """ @@ -948,10 +954,9 @@ class Message(ChatGetter, SenderGetter): this `forward_to` method. Use a `telethon.client.telegramclient.TelegramClient` instance directly. """ - if self._client: - kwargs['messages'] = self.id - kwargs['from_peer'] = await self.get_input_chat() - return await self._client.forward_messages(*args, **kwargs) + kwargs['messages'] = self.id + kwargs['from_peer'] = await self.get_input_chat() + return await self._client.forward_messages(*args, **kwargs) async def edit(self, *args, **kwargs): """ @@ -994,11 +999,10 @@ class Message(ChatGetter, SenderGetter): this `delete` method. Use a `telethon.client.telegramclient.TelegramClient` instance directly. """ - if self._client: - return await self._client.delete_messages( - await self.get_input_chat(), [self.id], - *args, **kwargs - ) + return await self._client.delete_messages( + await self.get_input_chat(), [self.id], + *args, **kwargs + ) async def download_media(self, *args, **kwargs): """ @@ -1006,10 +1010,9 @@ class Message(ChatGetter, SenderGetter): for `telethon.client.downloads.DownloadMethods.download_media` with the ``message`` already set. """ - if self._client: - # Passing the entire message is important, in case it has to be - # refetched for a fresh file reference. - return await self._client.download_media(self, *args, **kwargs) + # Passing the entire message is important, in case it has to be + # refetched for a fresh file reference. + return await self._client.download_media(self, *args, **kwargs) async def click(self, i=None, j=None, *, text=None, filter=None, data=None, share_phone=None, @@ -1117,9 +1120,6 @@ class Message(ChatGetter, SenderGetter): # Click on a button requesting a phone await message.click(0, share_phone=True) """ - if not self._client: - return - if data: chat = await self.get_input_chat() if not chat: @@ -1209,9 +1209,8 @@ class Message(ChatGetter, SenderGetter): ` with both ``entity`` and ``message`` already set. """ - if self._client: - await self._client.mark_read( - await self.get_input_chat(), max_id=self.id) + await self._client.mark_read( + await self.get_input_chat(), max_id=self.id) async def pin(self, *, notify=False, pm_oneside=False): """ @@ -1222,9 +1221,8 @@ class Message(ChatGetter, SenderGetter): # TODO Constantly checking if client is a bit annoying, # maybe just make it illegal to call messages from raw API? # That or figure out a way to always set it directly. - if self._client: - return await self._client.pin_message( - await self.get_input_chat(), self.id, notify=notify, pm_oneside=pm_oneside) + return await self._client.pin_message( + await self.get_input_chat(), self.id, notify=notify, pm_oneside=pm_oneside) async def unpin(self): """ @@ -1232,9 +1230,8 @@ class Message(ChatGetter, SenderGetter): `telethon.client.messages.MessageMethods.unpin_message` with both ``entity`` and ``message`` already set. """ - if self._client: - return await self._client.unpin_message( - await self.get_input_chat(), self.id) + return await self._client.unpin_message( + await self.get_input_chat(), self.id) # endregion Public Methods @@ -1257,9 +1254,6 @@ class Message(ChatGetter, SenderGetter): Re-fetches this message to reload the sender and chat entities, along with their input versions. """ - if not self._client: - return - try: chat = await self.get_input_chat() if self.is_channel else None msg = await self._client.get_messages(chat, ids=self.id) @@ -1284,7 +1278,7 @@ class Message(ChatGetter, SenderGetter): """ Helper methods to set the buttons given the input sender and chat. """ - if self._client and isinstance(self.reply_markup, ( + if isinstance(self.reply_markup, ( _tl.ReplyInlineMarkup, _tl.ReplyKeyboardMarkup)): self._buttons = [[ MessageButton(self._client, button, chat, bot, self.id) @@ -1300,7 +1294,7 @@ class Message(ChatGetter, SenderGetter): to know what bot we want to start. Raises ``ValueError`` if the bot cannot be found but is needed. Returns `None` if it's not needed. """ - if self._client and not isinstance(self.reply_markup, ( + if not isinstance(self.reply_markup, ( _tl.ReplyInlineMarkup, _tl.ReplyKeyboardMarkup)): return None From 9431e5cc3e024cc6024be1735f6f9e84ef5e7fe0 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 7 Feb 2022 10:29:44 +0100 Subject: [PATCH 197/256] Tidy up the migration guide some more --- readthedocs/misc/v2-migration-guide.rst | 218 +++++++++++++++++------- 1 file changed, 156 insertions(+), 62 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index c473ac76..9eec7301 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -75,6 +75,66 @@ removed. This implies: // TODO provide standalone alternative for this? +Overhaul of events and updates +------------------------------ + +Updates produced by the client are now also processed by your event handlers. +Before, if you had some code listening for new outgoing messages, only messages you sent with +another client, such as from Telegram Desktop, would be processed. Now, if your own code uses +``client.send_message``, you will also receive the new message event. Be careful, as this can +easily lead to "loops" (a new outgoing message can trigger ``client.send_message``, which +triggers a new outgoing message and the cycle repeats)! + +There are no longer "event builders" and "event" types. Now there are only events, and you +register the type of events you want, not an instance. Because of this, the way filters are +specified have also changed: + +.. code-block:: python + + # OLD + @client.on(events.NewMessage(chats=...)) + async def handler(event): + pass + + # NEW + @client.on(events.NewMessage, chats=...) + async def handler(event): # ^^ ^ + pass + +This also means filters are unified, although not all filters have an effect on all events types. +Type hinting is now done through ``events.NewMessage`` and not ``events.NewMessage.Event``. + +The filter rework also enables more features. For example, you can now mutate a ``chats`` filter +to add or remove a chat that needs to be received by a handler, rather than having to remove and +re-add the event handler. + +The ``from_users`` filter has been renamed to ``senders``. + +The ``inbox`` filter for ``events.MessageRead`` has been removed, in favour of ``outgoing`` and +``incoming``. + +``events.register``, ``events.unregister`` and ``events.is_handler`` have been removed. There is +no longer anything special about methods which are handlers, and they are no longer monkey-patched. +Because pre-defining the event type to handle without a client was useful, you can now instead use +the following syntax: + +.. code-block:: python + + # OLD + @events.register(events.NewMessage) + async def handler(event): + pass + + # NEW + async def handler(event: events.NewMessage): + pass # ^^^^^^^^^^^^^^^^^^^^^^^^ + +As a bonus, you only need to type-hint once, and both your IDE and Telethon will understand what +you meant. This is similar to Python's ``@dataclass`` which uses type hints. + +// TODO document filter creation and usage, showcase how to mutate them + + Complete overhaul of session files ---------------------------------- @@ -292,8 +352,8 @@ results into a list: // TODO does the download really need to be special? get download is kind of weird though -Raw API has been renamed and is now considered private ------------------------------------------------------- +Raw API has been renamed and is now immutable and considered private +-------------------------------------------------------------------- The subpackage holding the raw API methods has been renamed from ``tl`` to ``_tl`` in order to signal that these are prone to change across minor version bumps (the ``y`` in version ``x.y.z``). @@ -306,6 +366,10 @@ The ``Request`` suffix has been removed from the classes inside ``tl.functions`` The ``tl.types`` is now simply ``_tl``, and the ``tl.functions`` is now ``_tl.fn``. +Both the raw API types and functions are now immutable. This can enable optimizations in the +future, such as greatly reducing the number of intermediate objects created (something worth +doing for deeply-nested objects). + Some examples: .. code-block:: python @@ -375,7 +439,7 @@ The following modules have been moved inside ``_misc``: * ``helpers.py`` * ``hints.py`` * ``password.py`` -* ``requestiter.py` +* ``requestiter.py`` * ``statecache.py`` * ``utils.py`` @@ -413,6 +477,33 @@ Note that you do not need to ``await`` the call to ``.start()`` if you are going in a context-manager (but it's okay if you put the ``await``). +Changes to sending messages and files +------------------------------------- + +When sending messages or files, there is no longer a parse mode. Instead, the ``markdown`` or +``html`` parameters can be used instead of the (plaintext) ``message``. + +.. code-block:: python + + await client.send_message(chat, 'Default formatting (_markdown_)') + await client.send_message(chat, html='Force HTML formatting') + await client.send_message(chat, markdown='Force **Markdown** formatting') + +These 3 parameters are exclusive with each other (you can only use one). The goal here is to make +it consistent with the custom ``Message`` class, which also offers ``.markdown`` and ``.html`` +properties to obtain the correctly-formatted text, regardless of the default parse mode, and to +get rid of some implicit behaviour. It's also more convenient to set just one parameter than two +(the message and the parse mode separatedly). + +Although the goal is to reduce raw API exposure, ``formatting_entities`` stays, because it's the +only feasible way to manually specify them. + +When sending files, you can no longer pass a list of attributes. This was a common workaround to +set video size, audio duration, and so on. Now, proper parameters are available. The goal is to +hide raw API as much as possible (which lets the library hide future breaking changes as much as +possible). One can still use raw API if really needed. + + Several methods have been removed from the client ------------------------------------------------- @@ -458,8 +549,42 @@ The following ``utils`` methods no longer exist or have been made private: // TODO provide the new clean utils -Changes on how to configure filters for certain client methods --------------------------------------------------------------- +Changes to many friendly methods in the client +---------------------------------------------- + +Some of the parameters used to initialize the ``TelegramClient`` have been renamed to be clearer: + +* ``timeout`` is now ``connect_timeout``. +* ``connection_retries`` is now ``connect_retries``. +* ``retry_delay`` is now ``connect_retry_delay``. +* ``raise_last_call_error`` has been removed and is now the default. This means you won't get a + ``ValueError`` if an API call fails multiple times, but rather the original error. +* ``connection`` to change the connection mode has been removed for the time being. +* ``sequential_updates`` has been removed for the time being. + +// TODO document new parameters too + +``client.send_code_request`` no longer has ``force_sms`` (it was broken and was never reliable). + +``client.send_read_acknowledge`` is now ``client.mark_read``, consistent with the method of +``Message``, being shorter and less awkward to type. The method now only supports a single +message, not a list (the list was a lie, because all messages up to the one with the highest +ID were marked as read, meaning one could not leave unread gaps). ``max_id`` is now removed, +since it has the same meaning as the message to mark as read. The method no longer can clear +mentions without marking the chat as read, but this should not be an issue in practice. + +Every ``client.action`` can now be directly ``await``-ed, not just ``'cancel'``. + +``client.forward_messages`` now requires a list to be specified. The intention is to make it clear +that the method forwards message\ **s** and to reduce the number of strange allowed values, which +needlessly complicate the code. If you still need to forward a single message, manually construct +a list with ``[message]`` or use ``Message.forward_to``. + +``client.delete_messages`` now requires a list to be specified, with the same rationale as forward. + +``client.get_me`` no longer has an ``input_peer`` parameter. The goal is to hide raw API as much +as possible. Input peers are mostly an implementation detail the library needs to deal with +Telegram's API. Before, ``client.iter_participants`` (and ``get_participants``) would expect a type or instance of the raw Telegram definition as a ``filter``. Now, this ``filter`` expects a string. @@ -473,6 +598,20 @@ The supported values are: If you prefer to avoid hardcoding strings, you may use ``telethon.enums.Participant``. +The size selector for ``client.download_profile_photo`` and ``client.download_media` is now using +an enumeration: + +``` +from telethon import enums + +await client.download_profile_photo(user, thumb=enums.Size.ORIGINAL) +``` + +This new selection mode is also smart enough to pick the "next best" size if the specified one +is not available. The parameter is known as ``thumb`` and not ``size`` because documents don't +have a "size", they have thumbnails of different size. For profile photos, the thumbnail size is +also used. + // TODO maintain support for the old way of doing it? // TODO now that there's a custom filter, filter client-side for small chats? @@ -621,6 +760,18 @@ your handlers much more easily. // TODO provide standalone alternative for this? +Certain client properties and methods are now private or no longer exist +------------------------------------------------------------------------ + +The ``client.loop`` property has been removed. ``asyncio`` has been moving towards implicit loops, +so this is the next step. Async methods can be launched with the much simpler ``asyncio.run`` (as +opposed to the old ``client.loop.run_until_complete``). + +The ``client.upload_file`` method has been removed. It's a low-level method users should not need +to use. Its only purpose could have been to implement a cache of sorts, but this is something the +library needs to do, not the users. + + Deleting messages now returns a more useful value ------------------------------------------------- @@ -729,63 +880,6 @@ Now the URL is returned. You can still use ``webbrowser.open`` to get the old be --- -you can no longer pass an attributes list because the constructor is now nice. -use raw api if you really need it. -goal is to hide raw api from high level api. sorry. - -no parsemode. use the correct parameter. it's more convenient than setting two. - -formatting_entities stays because otherwise it's the only feasible way to manually specify it. - todo update send_message and send_file docs (well review all functions) album overhaul. use a list of Message instead. - -size selector for download_profile_photo and download_media is now different - -still thumb because otherwise documents are weird. - -keep support for explicit size instance? - -renamed send_read_acknowledge. add send_read_acknowledge as alias for mark_read? - -force sms removed as it was broken anyway and not very reliable - -you can now await client.action for a one-off any action not just cancel - -fwd msg and delete msg now mandate a list rather than a single int or msg -(since there's msg.delete and msg.forward_to this should be no issue). -they are meant to work on lists. - -also mark read only supports single now. a list would just be max anyway. -removed max id since it's not really of much use. - -client loop has been removed. embrace implicit loop as asyncio does now - -renamed some client params, and made other privates - timeout -> connect_timeout - connection_retries -> connect_retries - retry_delay -> connect_retry_delay - -sequential_updates is gone -connection type is gone - -raise_last_call_error is now the default rather than ValueError - -self-produced updates like getmessage now also trigger a handler - -input_peer removed from get_me; input peers should remain mostly an impl detail - -raw api types and fns are now immutable. this can enable optimizations in the future. - -upload_file has been removed from the public methods. it's a low-level method users should not need to use. - -events have changed. rather than differentiating between "event builder" and "event instance", instead there is only the instance, and you register the class. -where you had -@client.on(events.NewMessage(chats=...)) -it's now -@client.on(events.NewMessage, chats=...) -this also means filters are unified, although not all have an effect on all events. from_users renamed to senders. messageread inbox is gone in favor of outgoing/incoming. -events.register, unregister, is_handler and list are gone. now you can typehint instead. -def handler(event: events.NewMessage) -client.on, add, and remove have changed parameters/retval \ No newline at end of file From faaa038825f00bae5d7ff908e8eb5ab7ba3516f1 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 7 Feb 2022 11:01:18 +0100 Subject: [PATCH 198/256] Fix RpcError catch-all --- telethon/_client/auth.py | 2 +- telethon/errors/_rpcbase.py | 2 +- telethon/types/_custom/chatgetter.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/telethon/_client/auth.py b/telethon/_client/auth.py index 59f64933..a4942a64 100644 --- a/telethon/_client/auth.py +++ b/telethon/_client/auth.py @@ -368,7 +368,7 @@ async def qr_login(self: 'TelegramClient', ignored_ids: typing.List[int] = None) async def log_out(self: 'TelegramClient') -> bool: try: await self(_tl.fn.auth.LogOut()) - except errors.RPCError: + except errors.RpcError: return False await self.disconnect() diff --git a/telethon/errors/_rpcbase.py b/telethon/errors/_rpcbase.py index c67c6ebf..31691397 100644 --- a/telethon/errors/_rpcbase.py +++ b/telethon/errors/_rpcbase.py @@ -143,5 +143,5 @@ ServerError = _mk_error_type(code=500, doc=""" # Witnessed as -503 for "Timeout" BotTimeout = TimedOutError = _mk_error_type(code=503, doc=""" Clicking the inline buttons of bots that never (or take to long to) - call ``answerCallbackQuery`` will result in this "special" RPCError. + call ``answerCallbackQuery`` will result in this "special" RpcError. """) diff --git a/telethon/types/_custom/chatgetter.py b/telethon/types/_custom/chatgetter.py index c92ffb00..03306849 100644 --- a/telethon/types/_custom/chatgetter.py +++ b/telethon/types/_custom/chatgetter.py @@ -80,7 +80,7 @@ class ChatGetter(abc.ABC): self._chat = d.entity self._input_chat = d.input_entity break - except errors.RPCError: + except errors.RpcError: pass return self._input_chat From c220d50ed85bce8148a6fba52eb75026229e4475 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 7 Feb 2022 11:13:21 +0100 Subject: [PATCH 199/256] Add back _phone_code_hash There isn't really a better way to do this. --- telethon/_client/telegrambaseclient.py | 1 + 1 file changed, 1 insertion(+) diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 948c6776..9db125fd 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -141,6 +141,7 @@ def init( self._connect_timeout = connect_timeout self.flood_sleep_threshold = flood_sleep_threshold self._flood_waited_requests = {} # prevent calls that would floodwait entirely + self._phone_code_hash = {} # used during login to prevent exposing the hash to end users # Update handling. self._catch_up = catch_up From bd7675581d62c79af2591e9d37eb2c7d1ca4dbb7 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 7 Feb 2022 11:13:38 +0100 Subject: [PATCH 200/256] Make custom.Message properties mutable It is convenient to, for example, change the text before resending. --- telethon/types/_custom/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/types/_custom/message.py b/telethon/types/_custom/message.py index 24eed18f..c108b6ae 100644 --- a/telethon/types/_custom/message.py +++ b/telethon/types/_custom/message.py @@ -22,7 +22,7 @@ def _fwd(field, doc): return getattr(self._message, field, None) def fset(self, value): - setattr(self._message, field, value) + object.__setattr__(self._message, field, value) return property(fget, fset, None, doc) From 1f3ce0759428326d8c291b34f84905d2cd30eba5 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 7 Feb 2022 11:30:13 +0100 Subject: [PATCH 201/256] Manually set random_id in requests Getting the response message relies on this ID. Because requests are now immutable, and resolve sets the random_id, the method to obtain the response message would lack the random_id, as the original request is not touched by resolve. --- telethon/_client/messages.py | 10 +++++++--- telethon/types/_custom/inlineresult.py | 4 +++- telethon/types/_custom/messagebutton.py | 3 ++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/telethon/_client/messages.py b/telethon/_client/messages.py index e2065c7b..1949ee89 100644 --- a/telethon/_client/messages.py +++ b/telethon/_client/messages.py @@ -4,6 +4,7 @@ import time import typing import warnings import dataclasses +import os from .._misc import helpers, utils, requestiter, hints from ..types import _custom @@ -486,7 +487,8 @@ async def send_message( entity, message._file._media, reply_to_msg_id=reply_to, message=message._text, entities=message._fmt_entities, reply_markup=message._reply_markup, silent=message._silent, schedule_date=schedule, clear_draft=clear_draft, - background=background, noforwards=noforwards, send_as=send_as + background=background, noforwards=noforwards, send_as=send_as, + random_id=int.from_bytes(os.urandom(8), 'big', signed=True), ) else: request = _tl.fn.messages.SendMessage( @@ -501,7 +503,8 @@ async def send_message( reply_markup=_custom.button.build_reply_markup(buttons), schedule_date=schedule, noforwards=noforwards, - send_as=send_as + send_as=send_as, + random_id=int.from_bytes(os.urandom(8), 'big', signed=True), ) result = await self(request) @@ -574,7 +577,8 @@ async def forward_messages( with_my_score=with_my_score, schedule_date=schedule, noforwards=noforwards, - send_as=send_as + send_as=send_as, + random_id=[int.from_bytes(os.urandom(8), 'big', signed=True) for _ in chunk], ) result = await self(req) sent.extend(self._get_response_message(req, result, entity)) diff --git a/telethon/types/_custom/inlineresult.py b/telethon/types/_custom/inlineresult.py index 45867edb..84d8f64d 100644 --- a/telethon/types/_custom/inlineresult.py +++ b/telethon/types/_custom/inlineresult.py @@ -1,5 +1,6 @@ from ... import _tl from ..._misc import utils +import os class InlineResult: @@ -162,7 +163,8 @@ class InlineResult: clear_draft=clear_draft, hide_via=hide_via, reply_to_msg_id=reply_id, - send_as=send_as + send_as=send_as, + random_id=int.from_bytes(os.urandom(8), 'big', signed=True), ) return self._client._get_response_message( req, await self._client(req), entity) diff --git a/telethon/types/_custom/messagebutton.py b/telethon/types/_custom/messagebutton.py index eee4486e..ce5f7ed1 100644 --- a/telethon/types/_custom/messagebutton.py +++ b/telethon/types/_custom/messagebutton.py @@ -115,7 +115,8 @@ class MessageButton: return await self._client.get_entity(self.button.user_id) elif isinstance(self.button, _tl.KeyboardButtonSwitchInline): return await self._client(_tl.fn.messages.StartBot( - bot=self._bot, peer=self._chat, start_param=self.button.query + bot=self._bot, peer=self._chat, start_param=self.button.query, + random_id=int.from_bytes(os.urandom(8), 'big', signed=True), )) elif isinstance(self.button, _tl.KeyboardButtonUrl): return self.button.url From 84b016cf1c5384385db09e20cd034e6b9bbd131e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 8 Feb 2022 10:02:59 +0100 Subject: [PATCH 202/256] Make more TLObject methods private Even though raw API is somewhat necessary at times, these methods should remain implementation details. --- telethon/_client/users.py | 2 +- telethon/_crypto/rsa.py | 4 ++-- telethon/_misc/binaryreader.py | 2 +- telethon/_misc/tlobject.py | 12 ++++++------ telethon/_network/mtprotosender.py | 2 +- telethon/types/_core/gzippacked.py | 4 ++-- telethon/types/_core/messagecontainer.py | 2 +- telethon/types/_core/rpcresult.py | 6 +++--- telethon_generator/generators/tlobject.py | 14 +++++++------- 9 files changed, 24 insertions(+), 24 deletions(-) diff --git a/telethon/_client/users.py b/telethon/_client/users.py index b44564e1..d47fab67 100644 --- a/telethon/_client/users.py +++ b/telethon/_client/users.py @@ -40,7 +40,7 @@ async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sl for r in requests: if not isinstance(r, _tl.TLRequest): raise _NOT_A_REQUEST() - r = await r.resolve(self, utils) + r = await r._resolve(self, utils) # Avoid making the request if it's already in a flood wait if r.CONSTRUCTOR_ID in self._flood_waited_requests: diff --git a/telethon/_crypto/rsa.py b/telethon/_crypto/rsa.py index eca09743..fcfaca48 100644 --- a/telethon/_crypto/rsa.py +++ b/telethon/_crypto/rsa.py @@ -41,8 +41,8 @@ def _compute_fingerprint(key): :param key: the Crypto.RSA key. :return: its 8-bytes-long fingerprint. """ - n = tlobject.TLObject.serialize_bytes(get_byte_array(key.n)) - e = tlobject.TLObject.serialize_bytes(get_byte_array(key.e)) + n = tlobject.TLObject._serialize_bytes(get_byte_array(key.n)) + e = tlobject.TLObject._serialize_bytes(get_byte_array(key.e)) # Telegram uses the last 8 bytes as the fingerprint return struct.unpack(' Date: Tue, 8 Feb 2022 10:23:55 +0100 Subject: [PATCH 203/256] Remove client.edit_folder Not happy with the design, and keeping it would mean having to maintain it. It can be added back with a better design. --- readthedocs/misc/v2-migration-guide.rst | 6 ++ .../quick-references/client-reference.rst | 1 - telethon/_client/dialogs.py | 37 ----------- telethon/_client/telegramclient.py | 66 ------------------- telethon/types/_custom/dialog.py | 31 --------- 5 files changed, 6 insertions(+), 135 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 9eec7301..23eddcc8 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -771,6 +771,12 @@ The ``client.upload_file`` method has been removed. It's a low-level method user to use. Its only purpose could have been to implement a cache of sorts, but this is something the library needs to do, not the users. +The methods to deal with folders have been removed. The goal is to find and offer a better +interface to deal with both folders and archived chats in the future if there is demand for it. +This includes the removal of ``client.edit_folder``, ``Dialog.archive``, ``Dialog.archived``, and +the ``archived`` parameter of ``client.get_dialogs``. The ``folder`` parameter remains as it's +unlikely to change. + Deleting messages now returns a more useful value ------------------------------------------------- diff --git a/readthedocs/quick-references/client-reference.rst b/readthedocs/quick-references/client-reference.rst index 22517288..a713907f 100644 --- a/readthedocs/quick-references/client-reference.rst +++ b/readthedocs/quick-references/client-reference.rst @@ -103,7 +103,6 @@ Dialogs iter_dialogs get_dialogs - edit_folder iter_drafts get_drafts delete_dialog diff --git a/telethon/_client/dialogs.py b/telethon/_client/dialogs.py index e0f32547..fc5f2447 100644 --- a/telethon/_client/dialogs.py +++ b/telethon/_client/dialogs.py @@ -150,11 +150,7 @@ def get_dialogs( 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 - return _DialogsIter( self, limit, @@ -180,39 +176,6 @@ def get_drafts( return _DraftsIter(self, limit, entities=entity) -async def edit_folder( - self: 'TelegramClient', - entity: 'hints.EntitiesLike' = None, - folder: typing.Union[int, typing.Sequence[int]] = None, - *, - unpack=None -) -> _tl.Updates: - 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(_tl.fn.folders.DeleteFolder( - folder_id=unpack - )) - - 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(_tl.fn.folders.EditPeerFolders([ - _tl.InputFolderPeer(x, folder_id=y) - for x, y in zip(entities, folder) - ])) - async def delete_dialog( self: 'TelegramClient', entity: 'hints.EntityLike', diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 6a6cc1d9..23babf83 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -1390,7 +1390,6 @@ class TelegramClient: ignore_pinned: bool = False, ignore_migrated: bool = False, folder: int = None, - archived: bool = None ) -> dialogs._DialogsIter: """ Iterator over the dialogs (open conversations/subscribed channels). @@ -1445,10 +1444,6 @@ class TelegramClient: 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 `. @@ -1469,11 +1464,9 @@ class TelegramClient: # Getting only non-archived dialogs (both equivalent) non_archived = await client.get_dialogs(folder=0, limit=None) - non_archived = await client.get_dialogs(archived=False, limit=None) # Getting only archived dialogs (both equivalent) archived = await client.get_dialogs(folder=1, limit=None) - archived = await client.get_dialogs(archived=True, limit=None) """ @forward_call(dialogs.get_drafts) @@ -1510,65 +1503,6 @@ class TelegramClient: print(draft.text) """ - @forward_call(dialogs.edit_folder) - async def edit_folder( - self: 'TelegramClient', - entity: 'hints.EntitiesLike' = None, - folder: typing.Union[int, typing.Sequence[int]] = None, - *, - unpack=None - ) -> _tl.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) - """ - @forward_call(dialogs.delete_dialog) async def delete_dialog( self: 'TelegramClient', diff --git a/telethon/types/_custom/dialog.py b/telethon/types/_custom/dialog.py index 5f7d8d82..2cbf3dad 100644 --- a/telethon/types/_custom/dialog.py +++ b/telethon/types/_custom/dialog.py @@ -20,9 +20,6 @@ class Dialog: folder_id (`folder_id`): The folder ID that this dialog belongs to. - archived (`bool`): - Whether this dialog is archived or not (``folder_id is None``). - message (`Message `): The last message sent on this dialog. Note that this member will not be updated when new messages arrive, it's only set @@ -79,7 +76,6 @@ class Dialog: self.dialog = dialog self.pinned = bool(dialog.pinned) self.folder_id = dialog.folder_id - self.archived = dialog.folder_id is not None self.message = message self.date = getattr(self.message, 'date', None) @@ -122,33 +118,6 @@ class Dialog: # or it would raise `PEER_ID_INVALID`). await self._client.delete_dialog(self.entity, revoke=revoke) - async def archive(self, folder=1): - """ - Archives (or un-archives) this dialog. - - Args: - folder (`int`, optional): - The folder to which the dialog should be archived to. - - If you want to "un-archive" it, use ``folder=0``. - - Returns: - The :tl:`Updates` object that the request produces. - - Example: - - .. code-block:: python - - # Archiving - dialog.archive() - - # Un-archiving - dialog.archive(0) - """ - return await self._client(_tl.fn.folders.EditPeerFolders([ - _tl.InputFolderPeer(self.input_entity, folder_id=folder) - ])) - def to_dict(self): return { '_': 'Dialog', From 9b4808a558253d9fc2918172fd0468abb49b8c01 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 8 Feb 2022 11:31:24 +0100 Subject: [PATCH 204/256] Remove PackedChat In favour of using the session entity type consistently. --- telethon/_client/users.py | 4 +- telethon/_sessions/abstract.py | 6 +-- telethon/_sessions/memory.py | 6 +-- telethon/_sessions/sqlite.py | 8 ++-- telethon/_sessions/types.py | 70 ++++++++++++++++++++++++++++++-- telethon/_updates/__init__.py | 2 +- telethon/_updates/entitycache.py | 62 +++------------------------- telethon/_updates/messagebox.py | 2 +- 8 files changed, 86 insertions(+), 74 deletions(-) diff --git a/telethon/_client/users.py b/telethon/_client/users.py index d47fab67..4717d6b1 100644 --- a/telethon/_client/users.py +++ b/telethon/_client/users.py @@ -241,11 +241,11 @@ async def get_input_entity( entity = await self._session.get_entity(None, peer_id) if entity: if entity.ty in (Entity.USER, Entity.BOT): - return _tl.InputPeerUser(entity.id, entity.access_hash) + return _tl.InputPeerUser(entity.id, entity.hash) elif entity.ty in (Entity.GROUP): return _tl.InputPeerChat(peer.chat_id) elif entity.ty in (Entity.CHANNEL, Entity.MEGAGROUP, Entity.GIGAGROUP): - return _tl.InputPeerChannel(entity.id, entity.access_hash) + return _tl.InputPeerChannel(entity.id, entity.hash) # Only network left to try if isinstance(peer, str): diff --git a/telethon/_sessions/abstract.py b/telethon/_sessions/abstract.py index cdb747a4..328998f3 100644 --- a/telethon/_sessions/abstract.py +++ b/telethon/_sessions/abstract.py @@ -64,15 +64,15 @@ class Session(ABC): Get the `Entity` with matching ``ty`` and ``id``. The following groups of ``ty`` should be treated to be equivalent, that is, for a given - ``ty`` and ``id``, if the ``ty`` is in a given group, a matching ``access_hash`` with - that ``id`` from within any ``ty`` in that group should be returned. + ``ty`` and ``id``, if the ``ty`` is in a given group, a matching ``hash`` with that ``id`` + from within any ``ty`` in that group should be returned. * `EntityType.USER` and `EntityType.BOT`. * `EntityType.GROUP`. * `EntityType.CHANNEL`, `EntityType.MEGAGROUP` and `EntityType.GIGAGROUP`. For example, if a ``ty`` representing a bot is stored but the asking ``ty`` is a user, - the corresponding ``access_hash`` should still be returned. + the corresponding ``hash`` should still be returned. You may use ``EntityType.canonical`` to find out the canonical type. diff --git a/telethon/_sessions/memory.py b/telethon/_sessions/memory.py index 1c86aff7..7e87b194 100644 --- a/telethon/_sessions/memory.py +++ b/telethon/_sessions/memory.py @@ -34,12 +34,12 @@ class MemorySession(Session): return list(self.channel_states.values()) async def insert_entities(self, entities: List[Entity]): - self.entities.update((e.id, (e.ty, e.access_hash)) for e in entities) + self.entities.update((e.id, (e.ty, e.hash)) for e in entities) async def get_entity(self, ty: Optional[int], id: int) -> Optional[Entity]: try: - ty, access_hash = self.entities[id] - return Entity(ty, id, access_hash) + ty, hash = self.entities[id] + return Entity(ty, id, hash) except KeyError: return None diff --git a/telethon/_sessions/sqlite.py b/telethon/_sessions/sqlite.py index 2ea419be..7b0e5849 100644 --- a/telethon/_sessions/sqlite.py +++ b/telethon/_sessions/sqlite.py @@ -127,7 +127,7 @@ class SQLiteSession(Session): limit 1 ''') c.execute(''' - insert into entity (id, access_hash, ty) + insert into entity (id, hash, ty) select case when id < -1000000000000 then -(id + 1000000000000) @@ -173,7 +173,7 @@ class SQLiteSession(Session): )''', '''entity ( id integer primary key, - access_hash integer not null, + hash integer not null, ty integer not null )''', ) @@ -245,13 +245,13 @@ class SQLiteSession(Session): try: c.executemany( 'insert or replace into entity values (?,?,?)', - [(e.id, e.access_hash, e.ty) for e in entities] + [(e.id, e.hash, e.ty) for e in entities] ) finally: c.close() async def get_entity(self, ty: Optional[int], id: int) -> Optional[Entity]: - row = self._execute('select ty, id, access_hash from entity where id = ?', id) + row = self._execute('select ty, id, hash from entity where id = ?', id) return Entity(*row) if row else None async def save(self): diff --git a/telethon/_sessions/types.py b/telethon/_sessions/types.py index a9738709..3aca03e4 100644 --- a/telethon/_sessions/types.py +++ b/telethon/_sessions/types.py @@ -105,12 +105,74 @@ class Entity: """ Stores the information needed to use a certain user, chat or channel with the API. - * ty: 8-bit number indicating the type of the entity. + * ty: 8-bit number indicating the type of the entity (of type `EntityType`). * id: 64-bit number uniquely identifying the entity among those of the same type. - * access_hash: 64-bit number needed to use this entity with the API. + * hash: 64-bit signed number needed to use this entity with the API. + + The string representation of this class is considered to be stable, for as long as + Telegram doesn't need to add more fields to the entities. It can also be converted + to bytes with ``bytes(entity)``, for a more compact representation. """ - __slots__ = ('ty', 'id', 'access_hash') + __slots__ = ('ty', 'id', 'hash') ty: EntityType id: int - access_hash: int + hash: int + + @property + def is_user(self): + """ + ``True`` if the entity is either a user or a bot. + """ + return self.ty in (EntityType.USER, EntityType.BOT) + + @property + def is_group(self): + """ + ``True`` if the entity is a small group chat or `megagroup`_. + + .. _megagroup: https://telegram.org/blog/supergroups5k + """ + return self.ty in (EntityType.GROUP, EntityType.MEGAGROUP) + + @property + def is_channel(self): + """ + ``True`` if the entity is a broadcast channel or `broadcast group`_. + + .. _broadcast group: https://telegram.org/blog/autodelete-inv2#groups-with-unlimited-members + """ + return self.ty in (EntityType.CHANNEL, EntityType.GIGAGROUP) + + @classmethod + def from_str(cls, string: str): + """ + Convert the string into an `Entity`. + """ + try: + ty, id, hash = string.split('.') + ty, id, hash = ord(ty), int(id), int(hash) + except AttributeError: + raise TypeError(f'expected str, got {string!r}') from None + except (TypeError, ValueError): + raise ValueError(f'malformed entity str (must be T.id.hash), got {string!r}') from None + + return cls(EntityType(ty), id, hash) + + @classmethod + def from_bytes(cls, blob): + """ + Convert the bytes into an `Entity`. + """ + try: + ty, id, hash = struct.unpack(' (hash, ty) @@ -72,8 +19,11 @@ class EntityCache: self.self_bot = bot def get(self, id): - value = self.hash_map.get(id) - return PackedChat(ty=value[1], id=id, hash=value[0]) if value else None + try: + hash, ty = self.hash_map[id] + return Entity(ty, id, hash) + except KeyError: + return None def extend(self, users, chats): # See https://core.telegram.org/api/min for "issues" with "min constructors". @@ -100,4 +50,4 @@ class EntityCache: return [Entity(ty, id, hash) for id, (hash, ty) in self.hash_map.items()] def put(self, entity): - self.hash_map[entity.id] = (entity.access_hash, entity.ty) + self.hash_map[entity.id] = (entity.hash, entity.ty) diff --git a/telethon/_updates/messagebox.py b/telethon/_updates/messagebox.py index d2bdd174..d25e0d09 100644 --- a/telethon/_updates/messagebox.py +++ b/telethon/_updates/messagebox.py @@ -530,7 +530,7 @@ class MessageBox: return _tl.fn.updates.GetChannelDifference( force=False, - channel=packed.try_to_input_channel(), + channel=_tl.InputChannel(packed.id, packed.hash), filter=_tl.ChannelMessagesFilterEmpty(), pts=state.pts, limit=BOT_CHANNEL_DIFF_LIMIT if chat_hashes.self_bot else USER_CHANNEL_DIFF_LIMIT From 8df66c0b47ca06c06d8194d651267ea7d47939c3 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 8 Feb 2022 11:40:40 +0100 Subject: [PATCH 205/256] Add markdown and html properties to Message --- readthedocs/misc/v2-migration-guide.rst | 6 ----- telethon/types/_custom/message.py | 36 ++++++++++++++++++------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 23eddcc8..16023b67 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -639,12 +639,6 @@ The message sender no longer is the channel when no sender is provided by Telegr to patch this value for channels to be the same as the chat, but now it will be faithful to Telegram's value. -In order to avoid breaking more code than strictly necessary, ``.raw_text`` will remain a synonym -of ``.message``, and ``.text`` will still be the text formatted through the ``client.parse_mode``. -However, you're encouraged to change uses of ``.raw_text`` with ``.message``, and ``.text`` with -either ``.md_text`` or ``.html_text`` as needed. This is because both ``.text`` and ``.raw_text`` -may disappear in future versions, and their behaviour is not immediately obvious. - // TODO actually provide the things mentioned here diff --git a/telethon/types/_custom/message.py b/telethon/types/_custom/message.py index c108b6ae..730d2b49 100644 --- a/telethon/types/_custom/message.py +++ b/telethon/types/_custom/message.py @@ -9,7 +9,7 @@ from .file import File from .inputfile import InputFile from .inputmessage import InputMessage from .button import build_reply_markup -from ..._misc import utils, helpers, tlobject +from ..._misc import utils, helpers, tlobject, markdown, html from ... import _tl, _misc @@ -435,7 +435,6 @@ class Message(ChatGetter, SenderGetter): self._message = message # Convenient storage for custom functions - self._text = None self._file = None self._reply_message = None self._buttons = None @@ -533,8 +532,8 @@ class Message(ChatGetter, SenderGetter): @property def text(self): """ - The message text, formatted using the client's default - parse mode. Will be `None` for :tl:`MessageService`. + The message text, formatted using the default parse mode. + Will be `None` for :tl:`MessageService`. """ return InputMessage._default_parse_mode[1](self.message, self.entities) @@ -545,11 +544,9 @@ class Message(ChatGetter, SenderGetter): @property def raw_text(self): """ - The raw message text, ignoring any formatting. - Will be `None` for :tl:`MessageService`. + The plain message text, ignoring any formatting. Will be `None` for :tl:`MessageService`. - Setting a value to this field will erase the - `entities`, unlike changing the `message` member. + Setting a value to this field will erase the `entities`, unlike changing the `message` member. """ return self.message @@ -557,7 +554,28 @@ class Message(ChatGetter, SenderGetter): def raw_text(self, value): self.message = value self.entities = [] - self._text = None + + @property + def markdown(self): + """ + The message text, formatted using markdown. Will be `None` for :tl:`MessageService`. + """ + return markdown.unparse(self.message, self.entities) + + @markdown.setter + def markdown(self, value): + self.message, self.entities = markdown.parse(value) + + @property + def html(self): + """ + The message text, formatted using HTML. Will be `None` for :tl:`MessageService`. + """ + return html.unparse(self.message, self.entities) + + @html.setter + def html(self, value): + self.message, self.entities = html.parse(value) @property def is_reply(self): From fedbfedfd5aacc5b3a9f859a4c4f5f5a1d668782 Mon Sep 17 00:00:00 2001 From: Md Jisan <66010486+Jisan09@users.noreply.github.com> Date: Thu, 10 Feb 2022 18:41:05 +0530 Subject: [PATCH 206/256] Fix TypeError when loading StringSession (#3714) --- telethon/_sessions/string.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/_sessions/string.py b/telethon/_sessions/string.py index 2cb66aa6..7247272e 100644 --- a/telethon/_sessions/string.py +++ b/telethon/_sessions/string.py @@ -48,7 +48,7 @@ class StringSession(MemorySession): takeout_id=0 ) if ip_len == 4: - ipv4 = int.from_bytes(ip, 'big', False) + ipv4 = int.from_bytes(ip, 'big', signed=False) ipv6 = None else: ipv4 = None From a00f74285cf98e2672897ea914163a5067f1caf8 Mon Sep 17 00:00:00 2001 From: Devesh Pal Date: Fri, 11 Feb 2022 01:58:52 +0530 Subject: [PATCH 207/256] Document more RpcErrors (#3716) --- telethon_generator/data/errors.csv | 7 +++++++ telethon_generator/data/methods.csv | 8 ++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/telethon_generator/data/errors.csv b/telethon_generator/data/errors.csv index cc117c92..c5d8d0ab 100644 --- a/telethon_generator/data/errors.csv +++ b/telethon_generator/data/errors.csv @@ -57,7 +57,9 @@ CALL_PEER_INVALID,400,The provided call peer object is invalid CALL_PROTOCOL_FLAGS_INVALID,400,Call protocol flags invalid CDN_METHOD_INVALID,400,This method cannot be invoked on a CDN server. Refer to https://core.telegram.org/cdn#schema for available methods CHANNELS_ADMIN_PUBLIC_TOO_MUCH,400,"You're admin of too many public channels, make some channels private to change the username of this channel" +CHANNELS_ADMIN_LOCATED_TOO_MUCH,400, CHANNELS_TOO_MUCH,400,You have joined too many channels/supergroups +CHANNEL_ADD_INVALID,400, CHANNEL_BANNED,400,The channel is banned CHANNEL_INVALID,400,"Invalid channel object. Make sure to pass the right types, for instance making sure that the request is designed for channels or otherwise look for a different one more suited" CHANNEL_PRIVATE,400,The channel specified is private and you lack permission to access it. Another reason may be that you were banned from it @@ -345,12 +347,15 @@ SRP_ID_INVALID,400, START_PARAM_EMPTY,400,The start parameter is empty START_PARAM_INVALID,400,Start parameter invalid STATS_MIGRATE_0,303,The channel statistics must be fetched from DC {dc} +STICKERPACK_STICKERS_TOO_MUCH,400, STICKERSET_INVALID,400,The provided sticker set is invalid STICKERSET_OWNER_ANONYMOUS,406,This sticker set can't be used as the group's official stickers because it was created by one of its anonymous admins STICKERS_EMPTY,400,No sticker provided +STICKERS_TOO_MUCH,400, STICKER_DOCUMENT_INVALID,400,"The sticker file was invalid (this file has failed Telegram internal checks, make sure to use the correct format and comply with https://core.telegram.org/animated_stickers)" STICKER_EMOJI_INVALID,400,Sticker emoji invalid STICKER_FILE_INVALID,400,Sticker file invalid +STICKER_GIF_DIMENSIONS,400, STICKER_ID_INVALID,400,The provided sticker ID is invalid STICKER_INVALID,400,The provided sticker is invalid STICKER_PNG_DIMENSIONS,400,Sticker png dimensions invalid @@ -359,6 +364,8 @@ STICKER_TGS_NODOC,400, STICKER_TGS_NOTGS,400,Stickers must be a tgs file but the used file was not a tgs STICKER_THUMB_PNG_NOPNG,400,Stickerset thumb must be a png file but the used file was not png STICKER_THUMB_TGS_NOTGS,400,Stickerset thumb must be a tgs file but the used file was not tgs +STICKER_VIDEO_NOWEBM,400, +STICKER_VIDEO_BIG,400, STORAGE_CHECK_FAILED,500,Server storage check failed STORE_INVALID_SCALAR_TYPE,500, TAKEOUT_INIT_DELAY_0,420,A wait of {seconds} seconds is required before being able to initiate the takeout diff --git a/telethon_generator/data/methods.csv b/telethon_generator/data/methods.csv index 3088b65b..f75abb9b 100644 --- a/telethon_generator/data/methods.csv +++ b/telethon_generator/data/methods.csv @@ -90,13 +90,13 @@ bots.sendCustomRequest,bot,USER_BOT_INVALID bots.setBotCommands,bot,BOT_COMMAND_DESCRIPTION_INVALID BOT_COMMAND_INVALID LANG_CODE_INVALID channels.checkUsername,user,CHANNEL_INVALID CHAT_ID_INVALID USERNAME_INVALID channels.convertToGigagroup,user,PARTICIPANTS_TOO_FEW -channels.createChannel,user,CHAT_TITLE_EMPTY USER_RESTRICTED +channels.createChannel,user,CHANNELS_ADMIN_LOCATED_TOO_MUCH CHAT_TITLE_EMPTY USER_RESTRICTED channels.deleteChannel,user,CHANNEL_INVALID CHANNEL_PRIVATE CHANNEL_TOO_LARGE channels.deleteHistory,user, channels.deleteMessages,both,CHANNEL_INVALID CHANNEL_PRIVATE MESSAGE_DELETE_FORBIDDEN channels.deleteUserHistory,user,CHANNEL_INVALID CHAT_ADMIN_REQUIRED channels.editAdmin,both,ADMINS_TOO_MUCH ADMIN_RANK_EMOJI_NOT_ALLOWED ADMIN_RANK_INVALID BOT_CHANNELS_NA CHANNEL_INVALID CHAT_ADMIN_INVITE_REQUIRED CHAT_ADMIN_REQUIRED FRESH_CHANGE_ADMINS_FORBIDDEN RIGHT_FORBIDDEN USER_CREATOR USER_ID_INVALID USER_NOT_MUTUAL_CONTACT USER_PRIVACY_RESTRICTED -channels.editBanned,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED USER_ADMIN_INVALID USER_ID_INVALID +channels.editBanned,both,CHANNEL_INVALID CHANNEL_ADD_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED USER_ADMIN_INVALID USER_ID_INVALID channels.editCreator,user,PASSWORD_MISSING PASSWORD_TOO_FRESH_0 SESSION_TOO_FRESH_0 SRP_ID_INVALID channels.editLocation,user, channels.editPhoto,both,CHANNEL_INVALID CHAT_ADMIN_REQUIRED FILE_REFERENCE_INVALID PHOTO_INVALID @@ -341,10 +341,10 @@ setClientDHParams,both, stats.getBroadcastStats,user,BROADCAST_REQUIRED CHAT_ADMIN_REQUIRED CHP_CALL_FAIL STATS_MIGRATE_0 stats.getMegagroupStats,user,CHAT_ADMIN_REQUIRED MEGAGROUP_REQUIRED STATS_MIGRATE_0 stats.loadAsyncGraph,user,GRAPH_INVALID_RELOAD GRAPH_OUTDATED_RELOAD -stickers.addStickerToSet,bot,BOT_MISSING STICKERSET_INVALID STICKER_PNG_NOPNG STICKER_TGS_NOTGS +stickers.addStickerToSet,bot,BOT_MISSING STICKERS_TOO_MUCH STICKERSET_INVALID STICKERPACK_STICKERS_TOO_MUCH STICKER_PNG_NOPNG STICKER_TGS_NOTGS stickers.changeStickerPosition,bot,BOT_MISSING STICKER_INVALID stickers.checkShortName,user,SHORT_NAME_INVALID SHORT_NAME_OCCUPIED -stickers.createStickerSet,bot,BOT_MISSING PACK_SHORT_NAME_INVALID PACK_SHORT_NAME_OCCUPIED PEER_ID_INVALID SHORTNAME_OCCUPY_FAILED STICKERS_EMPTY STICKER_EMOJI_INVALID STICKER_FILE_INVALID STICKER_PNG_DIMENSIONS STICKER_PNG_NOPNG STICKER_TGS_NOTGS STICKER_THUMB_PNG_NOPNG STICKER_THUMB_TGS_NOTGS USER_ID_INVALID +stickers.createStickerSet,bot,BOT_MISSING PACK_SHORT_NAME_INVALID PACK_SHORT_NAME_OCCUPIED PEER_ID_INVALID SHORTNAME_OCCUPY_FAILED STICKERS_EMPTY STICKER_EMOJI_INVALID STICKER_FILE_INVALID STICKER_GIF_DIMENSIONS STICKER_PNG_DIMENSIONS STICKER_PNG_NOPNG STICKER_TGS_NOTGS STICKER_THUMB_PNG_NOPNG STICKER_THUMB_TGS_NOTGS STICKER_VIDEO_BIG STICKER_VIDEO_NOWEBM USER_ID_INVALID stickers.removeStickerFromSet,bot,BOT_MISSING STICKER_INVALID stickers.setStickerSetThumb,bot,STICKER_THUMB_PNG_NOPNG STICKER_THUMB_TGS_NOTGS stickers.suggestShortName,user,TITLE_INVALID From ccedf4ad0b401ae4ee993d5d18322544a1c0d327 Mon Sep 17 00:00:00 2001 From: Christoph Berg Date: Sat, 12 Feb 2022 09:07:23 +0100 Subject: [PATCH 208/256] setup.py: Make build reproducible by sorting generator input (#3695) Debian's reproducible builds infrastructure reports that the ordering of objects listed in the tlobjects dict in /usr/lib/python3/dist-packages/telethon/tl/alltlobjects.py depends on the filesystem ordering of the *.tl files read at build time. Fix that by sorting the list in setup.py. Reproducible builds are desirable for security and auditing reasons, and easing QA. More details at https://reproducible-builds.org/ . --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index caf4a54a..373fc90c 100755 --- a/setup.py +++ b/setup.py @@ -54,7 +54,7 @@ METHODS_IN = GENERATOR_DIR / 'data/methods.csv' # Which raw API methods are covered by *friendly* methods in the client? FRIENDLY_IN = GENERATOR_DIR / 'data/friendly.csv' -TLOBJECT_IN_TLS = [Path(x) for x in GENERATOR_DIR.glob('data/*.tl')] +TLOBJECT_IN_TLS = [Path(x) for x in sorted(GENERATOR_DIR.glob('data/*.tl'))] TLOBJECT_OUT = LIBRARY_DIR / '_tl' TLOBJECT_MOD = 'telethon._tl' From af0ea638b44f183bd017f67299cb4598e70ec9ea Mon Sep 17 00:00:00 2001 From: Aditya Date: Wed, 16 Feb 2022 15:55:52 +0530 Subject: [PATCH 209/256] Use `asyncio` in place of `aio_loop` (#3724) Fixes #3717. --- telethon_examples/gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon_examples/gui.py b/telethon_examples/gui.py index 61027d52..ea6d248a 100644 --- a/telethon_examples/gui.py +++ b/telethon_examples/gui.py @@ -53,7 +53,7 @@ def callback(func): def wrapped(*args, **kwargs): result = func(*args, **kwargs) if inspect.iscoroutine(result): - aio_loop.create_task(result) + asyncio.create_task(result) return wrapped From c45b91910906736dd6f21b6a3bdd0dfa80a905cd Mon Sep 17 00:00:00 2001 From: Shrimadhav U K Date: Wed, 16 Feb 2022 15:56:09 +0530 Subject: [PATCH 210/256] Add a friendly method to react to messages (#3681) --- telethon/_client/messages.py | 41 +++++++++++++++++++++++++++++++ telethon/types/_custom/message.py | 13 ++++++++++ 2 files changed, 54 insertions(+) diff --git a/telethon/_client/messages.py b/telethon/_client/messages.py index 1949ee89..2e89ffd8 100644 --- a/telethon/_client/messages.py +++ b/telethon/_client/messages.py @@ -747,3 +747,44 @@ async def _pin(self, entity, message, *, unpin, notify=False, pm_oneside=False): # Pinning a message that doesn't exist would RPC-error earlier return self._get_response_message(request, result, entity) + +async def send_reaction( + self: 'TelegramClient', + entity: 'hints.EntityLike', + message: 'hints.MessageIDLike', + reaction: typing.Optional[str] = None, + big: bool = False +): + message = utils.get_message_id(message) or 0 + if not reaction: + get_default_request = _tl.fn.help.GetAppConfig() + app_config = await self(get_default_request) + reaction = ( + next( + ( + y for y in app_config.value + if "reactions_default" in y.key + ) + ) + ).value.value + request = _tl.fn.messages.SendReaction( + big=big, + peer=entity, + msg_id=message, + reaction=reaction + ) + result = await self(request) + for update in result.updates: + if isinstance(update, _tl.UpdateMessageReactions): + return update.reactions + if isinstance(update, _tl.UpdateEditMessage): + return update.message.reactions + +async def set_quick_reaction( + self: 'TelegramClient', + reaction: str +): + request = _tl.fn.messages.SetDefaultReaction( + reaction=reaction + ) + return await self(request) diff --git a/telethon/types/_custom/message.py b/telethon/types/_custom/message.py index 730d2b49..84c2e409 100644 --- a/telethon/types/_custom/message.py +++ b/telethon/types/_custom/message.py @@ -1251,6 +1251,19 @@ class Message(ChatGetter, SenderGetter): return await self._client.unpin_message( await self.get_input_chat(), self.id) + async def react(self, reaction=None): + """ + Reacts on the given message. Shorthand for + `telethon.client.messages.MessageMethods.send_reaction` + with both ``entity`` and ``message`` already set. + """ + if self._client: + return await self._client.send_reaction( + await self.get_input_chat(), + self.id, + reaction + ) + # endregion Public Methods # region Private Methods From 8aa4d9a26391f2990e4c1fd7efd218ece2f0f353 Mon Sep 17 00:00:00 2001 From: Mohammadreza Jafari <30555691+fristhon@users.noreply.github.com> Date: Wed, 16 Feb 2022 15:10:07 +0330 Subject: [PATCH 211/256] Make document title a required parameter (#3182) --- telethon/types/_custom/inlinebuilder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/telethon/types/_custom/inlinebuilder.py b/telethon/types/_custom/inlinebuilder.py index 3fea2fc2..39b20a1c 100644 --- a/telethon/types/_custom/inlinebuilder.py +++ b/telethon/types/_custom/inlinebuilder.py @@ -226,7 +226,7 @@ class InlineBuilder: # noinspection PyIncorrectDocstring async def document( - self, file, title=None, *, description=None, type=None, + self, file, title, *, description=None, type=None, mime_type=None, attributes=None, force_document=False, voice_note=False, video_note=False, use_cache=True, id=None, text=None, parse_mode=(), link_preview=True, @@ -245,7 +245,7 @@ class InlineBuilder: Same as ``file`` for `client.send_file() `. - title (`str`, optional): + title (`str`): The title to be shown for this result. description (`str`, optional): From 4fae119a18b8e5c55be6074e05fa4c8941f6ba89 Mon Sep 17 00:00:00 2001 From: "sandeep.n" <58665444+sandy1709@users.noreply.github.com> Date: Wed, 16 Feb 2022 20:29:56 +0530 Subject: [PATCH 212/256] Fix StringSession usage of IPv6 (#3726) --- telethon/_sessions/string.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/telethon/_sessions/string.py b/telethon/_sessions/string.py index 7247272e..e63376af 100644 --- a/telethon/_sessions/string.py +++ b/telethon/_sessions/string.py @@ -74,15 +74,15 @@ class StringSession(MemorySession): if not self.state: return '' - if self.state.ipv6 is not None: - ip = self.state.ipv6.to_bytes(16, 'big', signed=False) + if self.dcs[self.state.dc_id].ipv6 is not None: + ip = self.dcs[self.state.dc_id].ipv6.to_bytes(16, 'big', signed=False) else: - ip = self.state.ipv6.to_bytes(4, 'big', signed=False) + ip = self.dcs[self.state.dc_id].ipv6.to_bytes(4, 'big', signed=False) return CURRENT_VERSION + StringSession.encode(struct.pack( _STRUCT_PREFORMAT.format(len(ip)), self.state.dc_id, ip, - self.state.port, + self.dcs[self.state.dc_id].port, self.dcs[self.state.dc_id].auth )) From d1836ab8990cb1ca2c201738e8952210553b8691 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 9 Feb 2022 10:00:20 +0100 Subject: [PATCH 213/256] Review errors and remove some type checks If the type is not correct, we can let the API fail instead. This way, if the API supports more types in the future, there does not need to be an update. This has affected participant permissions in groups. --- telethon/_client/chats.py | 8 -------- telethon/_client/telegrambaseclient.py | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/telethon/_client/chats.py b/telethon/_client/chats.py index a9a4a45f..f17d72db 100644 --- a/telethon/_client/chats.py +++ b/telethon/_client/chats.py @@ -484,8 +484,6 @@ async def edit_admin( entity = await self.get_input_entity(entity) user = await self.get_input_entity(user) ty = helpers._entity_type(user) - if ty != helpers._EntityType.USER: - raise ValueError('You must pass a user entity') perm_names = ( 'change_info', 'post_messages', 'edit_messages', 'delete_messages', @@ -549,8 +547,6 @@ async def edit_permissions( pin_messages: bool = True) -> _tl.Updates: entity = await self.get_input_entity(entity) ty = helpers._entity_type(entity) - if ty != helpers._EntityType.CHANNEL: - raise ValueError('You must pass either a channel or a supergroup') rights = _tl.ChatBannedRights( until_date=until_date, @@ -592,8 +588,6 @@ async def kick_participant( ): entity = await self.get_input_entity(entity) user = await self.get_input_entity(user) - if helpers._entity_type(user) != helpers._EntityType.USER: - raise ValueError('You must pass a user entity') ty = helpers._entity_type(entity) if ty == helpers._EntityType.CHAT: @@ -660,8 +654,6 @@ async def get_stats( message: 'typing.Union[int, _tl.Message]' = None, ): entity = await self.get_input_entity(entity) - if helpers._entity_type(entity) != helpers._EntityType.CHANNEL: - raise TypeError('You must pass a channel entity') message = utils.get_message_id(message) if message is not None: diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 9db125fd..09b1db51 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -157,7 +157,7 @@ def init( if not api_id or not api_hash: raise ValueError( "Your API ID or Hash cannot be empty or None. " - "Refer to telethon.rtfd.io for more information.") + "Refer to docs.telethon.dev for more information.") if local_addr is not None: if use_ipv6 is False and ':' in local_addr: From 2c4ff8803f0ce0ac87ce15cf0d45ca3aae3a71eb Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 9 Feb 2022 10:31:04 +0100 Subject: [PATCH 214/256] Remove strange except in to_dict --- telethon/types/_custom/draft.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/telethon/types/_custom/draft.py b/telethon/types/_custom/draft.py index 46dc9c87..fba7f4d2 100644 --- a/telethon/types/_custom/draft.py +++ b/telethon/types/_custom/draft.py @@ -161,15 +161,10 @@ class Draft: return await self.set_message(text='') def to_dict(self): - try: - entity = self.entity - except RpcError as e: - entity = e - return { '_': 'Draft', 'text': self.text, - 'entity': entity, + 'entity': self.entity, 'date': self.date, 'link_preview': self.link_preview, 'reply_to_msg_id': self.reply_to_msg_id From 9bfe4cddf58aac28398ebcb4706ab8517ea5857a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 12 Feb 2022 11:29:28 +0100 Subject: [PATCH 215/256] Begin major entity overhaul Introduce User and Chat as better alternatives to raw API. Document the rationale and new intended usage. --- readthedocs/misc/v2-migration-guide.rst | 111 +++++++++- telethon/types/_custom/chat.py | 270 +++++++++++++++++++++++ telethon/types/_custom/user.py | 277 ++++++++++++++++++++++++ 3 files changed, 649 insertions(+), 9 deletions(-) create mode 100644 telethon/types/_custom/chat.py create mode 100644 telethon/types/_custom/user.py diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 16023b67..09618508 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -598,14 +598,14 @@ The supported values are: If you prefer to avoid hardcoding strings, you may use ``telethon.enums.Participant``. -The size selector for ``client.download_profile_photo`` and ``client.download_media` is now using +The size selector for ``client.download_profile_photo`` and ``client.download_media`` is now using an enumeration: -``` -from telethon import enums +.. code-block:: python -await client.download_profile_photo(user, thumb=enums.Size.ORIGINAL) -``` + from telethon import enums + + await client.download_profile_photo(user, thumb=enums.Size.ORIGINAL) This new selection mode is also smart enough to pick the "next best" size if the specified one is not available. The parameter is known as ``thumb`` and not ``size`` because documents don't @@ -639,7 +639,100 @@ The message sender no longer is the channel when no sender is provided by Telegr to patch this value for channels to be the same as the chat, but now it will be faithful to Telegram's value. -// TODO actually provide the things mentioned here + +Overhaul of users and chats are no longer raw API types +------------------------------------------------------- + +Users and chats are no longer raw API types. The goal is to reduce the amount of raw API exposed +to the user, and to provide less confusing naming. This also means that **the sender and chat of +messages and events is now a different type**. If you were using `isinstance` to check the types, +you will need to update that code. However, if you were accessing things like the ``first_name`` +or ``username``, you will be fine. + +Raw API is not affected by this change. When using it, the raw :tl:`User`, :tl:`Chat` and +:tl:`Channel` are still returned. + +For friendly methods and events, There are now two main entity types, `User` and `Chat`. +`User`\ s are active entities which can send messages and interact with eachother. There is an +account controlling them. `Chat`\ s are passive entities where multiple users can join and +interact with each other. This includes small groups, supergroups, and broadcast channels. + +``event.get_sender``, ``event.sender``, ``event.get_chat``, and ``event.chat`` (as well as +the same methods on ``message`` and elsewhere) now return this new type. The ``sender`` and +``chat`` is **now always returned** (where it makes sense, so no sender in channel messages), +even if Telegram did not include information about it in the update. This means you can use +send messages to ``event.chat`` without worrying if Telegram included this information or not, +or even access ``event.chat.id``. This was often a papercut. However, if you need other +information like the title, you might still need to use ``await event.get_chat()``, which is +used to signify an API call might be necessary. + +``event.get_input_sender``, ``event.input_sender``, ``message.get_input_sender`` and +``message.input_sender`` (among other variations) have been removed. Instead, a new ``compact`` +method has been added to the new `User` and `Chat` types, which can be used to obtain a compact +representation of the sender. The "input" terminology is confusing for end-users, as it's mostly +an implementation detail of friendly methods. Because the return type would've been different +had these methods been kept, one would have had to review code using them regardless. + +What this means is that, if you now want a compact way to store a user or chat for later use, +you should use ``compact``: + +.. code-block:: python + + compacted_user = message.sender.compact() + # store compacted_user in a database or elsewhere for later use + +Public methods accept this type as input parameters. This means you can send messages to a +compacted user or chat, for example. + +``event.is_private``, ``event.is_group`` and ``event.is_channel`` have **been removed** (among +other variations, such as in ``message``). It didn't make much sense to ask "is this event a +group", and there is no such thing as "group messages" currently either. Instead, it's sensible +to ask if the sender of a message is a group, or the chat of an event is a channel. New properties +have been added to both the `User` and `Chat` classes: + +* ``.is_user`` will always be `True` for `User` and `False` for `Chat`. +* ``.is_group`` will be `False` for `User` and be `True` for small group chats and supergroups. +* ``.is_broadcast`` will be `False` for `User` and `True` for broadcast channels and broadcast groups. + +Because the properties exist both in `User` and `Chat`, you do not need use `isinstance` to check +if a sender is a channel or if a chat is a user. + +Some fields of the new `User` type differ from the naming or value type of its raw API counterpart: + +* ``user.restriction_reason`` has been renamed to ``restriction_reasons`` (with a trailing **s**) + and now always returns a list. +* ``user.bot_chat_history`` has been renamed to ``user.bot_info.chat_history_access``. +* ``user.bot_nochats`` has been renamed to ``user.bot_info.private_only``. +* ``user.bot_inline_geo`` has been renamed to ``user.bot_info.inline_geo``. +* ``user.bot_info_version`` has been renamed to ``user.bot_info.version``. +* ``user.bot_inline_placeholder`` has been renamed to ``user.bot_info.inline_placeholder``. + +The new ``user.bot_info`` field will be `None` for non-bots. The goal is to unify where this +information is found and reduce clutter in the main ``user`` type. + +Some fields of the new `Chat` type differ from the naming or value type of its raw API counterpart: + +* ``chat.date`` is currently not available. It's either the chat creation or join date, but due + to this inconsistency, it's not included to allow for a better solution in the future. +* ``chat.has_link`` is currently not available, to allow for a better alternative in the future. +* ``chat.has_geo`` is currently not available, to allow for a better alternative in the future. +* ``chat.call_active`` is currently not available, until it's decided what to do about calls. +* ``chat.call_not_empty`` is currently not available, until it's decided what to do about calls. +* ``chat.version`` was removed. It's an implementation detail. +* ``chat.min`` was removed. It's an implementation detail. +* ``chat.deactivated`` was removed. It's redundant with ``chat.migrated_to``. +* ``chat.forbidden`` has been added as a replacement for ``isinstance(chat, (ChatForbidden, ChannelForbidden))``. +* ``chat.forbidden_until`` has been added as a replacement for ``until_date`` in forbidden chats. +* ``chat.restriction_reason`` has been renamed to ``restriction_reasons`` (with a trailing **s**) + and now always returns a list. +* ``chat.migrated_to`` no longer returns a raw type, and instead returns this new `Chat` type. + +If you have a need for these, please step in, and explain your use case, so we can work together +to implement a proper design. + +Both the new `User` and `Chat` types offer a ``fetch`` method, which can be used to refetch the +instance with fresh information, including the full information about the user (such as the user's +biography or a chat's about description). Using a flat list to define buttons will now create rows and not columns @@ -662,9 +755,9 @@ If you still want the old behaviour, wrap the list inside another list: bot.send_message(chat, message, buttons=[[ # + - Button.inline('top'), - Button.inline('middle'), - Button.inline('bottom'), + Button.inline('left'), + Button.inline('center'), + Button.inline('right'), ]]) #+ diff --git a/telethon/types/_custom/chat.py b/telethon/types/_custom/chat.py new file mode 100644 index 00000000..57593428 --- /dev/null +++ b/telethon/types/_custom/chat.py @@ -0,0 +1,270 @@ +from typing import Optional, List, TYPE_CHECKING +from datetime import datetime +import mimetypes +from .chatgetter import ChatGetter +from .sendergetter import SenderGetter +from .messagebutton import MessageButton +from .forward import Forward +from .file import File +from .inputfile import InputFile +from .inputmessage import InputMessage +from .button import build_reply_markup +from ..._misc import utils, helpers, tlobject, markdown, html +from ... import _tl, _misc + + +if TYPE_CHECKING: + from ..._misc import hints + + +def _fwd(field, doc): + def fget(self): + return getattr(self._message, field, None) + + def fset(self, value): + object.__setattr__(self._message, field, value) + + return property(fget, fset, None, doc) + + +class _InputChat: + """ + Input channels and peer chats use a different name for "id" which breaks the property forwarding. + + This class simply holds the two fields with proper names. + """ + __slots__ = ('id', 'access_hash') + + def __init__(self, input): + self.id = getattr(input, 'channel_id', None) or input.chat_id + self.access_hash = getattr(input, 'access_hash', None) + + +class Chat: + """ + Represents a :tl:`Chat` or :tl:`Channel` (or their empty and forbidden variants) from the API. + """ + + id = _fwd('id', """ + The chat identifier. This is the only property which will **always** be present. + """) + + title = _fwd('title', """ + The chat title. It will be `None` for empty chats. + """) + + username = _fwd('username', """ + The public `username` of the chat. + """) + + participants_count = _fwd('participants_count', """ + The number of participants who are currently joined to the chat. + It will be `None` for forbidden and empty chats or if the information isn't known. + """) + + broadcast = _fwd('broadcast', """ + `True` if the chat is a broadcast channel. + """) + + megagroup = _fwd('megagroup', """ + `True` if the chat is a supergroup. + """) + + gigagroup = _fwd('gigagroup', """ + `True` if the chat used to be a `megagroup` but is now a broadcast group. + """) + + verified = _fwd('verified', """ + `True` if the chat has been verified as official by Telegram. + """) + + scam = _fwd('scam', """ + `True` if the chat has been flagged as scam. + """) + + fake = _fwd('fake', """ + `True` if the chat has been flagged as fake. + """) + + creator = _fwd('creator', """ + `True` if the logged-in account is the creator of the chat. + """) + + kicked = _fwd('kicked', """ + `True` if the logged-in account was kicked from the chat. + """) + + left = _fwd('left', """ + `True` if the logged-in account has left the chat. + """) + + restricted = _fwd('restricted', """ + `True` if the logged-in account cannot write in the chat. + """) + + slowmode_enabled = _fwd('slowmode_enabled', """ + `True` if the chat currently has slowmode enabled. + """) + + signatures = _fwd('signatures', """ + `True` if signatures are enabled in a broadcast channel. + """) + + admin_rights = _fwd('admin_rights', """ + Administrator rights the logged-in account has in the chat. + """) + + banned_rights = _fwd('banned_rights', """ + Banned rights the logged-in account has in the chat. + """) + + default_banned_rights = _fwd('default_banned_rights', """ + The default banned rights for every non-admin user in the chat. + """) + + @property + def forbidden(self): + """ + `True` if access to this channel is forbidden. + """ + return isinstance(self._chat, (_tl.ChatForbidden, _tl.ChannelForbidden)) + + @property + def forbidden_until(self): + """ + If access to the chat is only temporarily `forbidden`, returns when access will be regained. + """ + try: + return self._chat.until_date + except AttributeError: + return None + + @property + def restriction_reasons(self): + """ + Returns a possibly-empty list of reasons why the chat is restricted to some platforms. + """ + try: + return self._chat.restriction_reason or [] + except AttributeError: + return [] + + @property + def migrated_to(self): + """ + If the current chat has migrated to a larger group, returns the new `Chat`. + """ + try: + migrated = self._chat.migrated_to + except AttributeError: + migrated = None + + return Chat(_InputChat(migrated), self._client) if migrated else None + + def __init__(self): + raise TypeError('You cannot create Chat instances by hand!') + + @classmethod + def _new(cls, client, chat): + self = cls.__new__(cls) + self._client = client + self._chat = chat + self._full = None + return self + + async def fetch(self, *, full=False): + """ + Perform an API call to fetch fresh information about this chat. + + Returns itself, but with the information fetched (allowing you to chain the call). + + If ``full`` is ``True``, the full information about the user will be fetched, + which will include things like ``about``. + """ + return self + + def compact(self): + """ + Return a compact representation of this user, useful for storing for later use. + """ + raise RuntimeError('TODO') + + @property + def client(self): + """ + Returns the `TelegramClient ` + which returned this user from a friendly method. + """ + return self._client + + def to_dict(self): + return self._user.to_dict() + + def __repr__(self): + return helpers.pretty_print(self) + + def __str__(self): + return helpers.pretty_print(self, max_depth=2) + + def stringify(self): + return helpers.pretty_print(self, indent=0) + + @property + def is_user(self): + """ + Returns `False`. + + This property also exists in `User`, where it returns `True`. + + .. code-block:: python + + if message.chat.is_user: + ... # do stuff + """ + return False + + @property + def is_group(self): + """ + Returns `True` if the chat is a small group chat or `megagroup`_. + + This property also exists in `User`, where it returns `False`. + + .. code-block:: python + + if message.chat.is_group: + ... # do stuff + + .. _megagroup: https://telegram.org/blog/supergroups5k + """ + return True + + @property + def is_broadcast(self): + """ + Returns `True` if the chat is a broadcast channel group chat or `broadcast group`_. + + This property also exists in `User`, where it returns `False`. + + .. code-block:: python + + if message.chat.is_broadcast: + ... # do stuff + + .. _broadcast group: https://telegram.org/blog/autodelete-inv2#groups-with-unlimited-members + """ + return True + + @property + def full_name(self): + """ + Returns `title`. + + This property also exists in `User`, where it returns the first name and last name + concatenated. + + .. code-block:: python + + print(message.chat.full_name): + """ + return self.title diff --git a/telethon/types/_custom/user.py b/telethon/types/_custom/user.py new file mode 100644 index 00000000..63b3fccb --- /dev/null +++ b/telethon/types/_custom/user.py @@ -0,0 +1,277 @@ +from typing import Optional, List, TYPE_CHECKING +from datetime import datetime +import mimetypes +from .chatgetter import ChatGetter +from .sendergetter import SenderGetter +from .messagebutton import MessageButton +from .forward import Forward +from .file import File +from .inputfile import InputFile +from .inputmessage import InputMessage +from .button import build_reply_markup +from ..._misc import utils, helpers, tlobject, markdown, html +from ... import _tl, _misc + + +if TYPE_CHECKING: + from ..._misc import hints + + +def _fwd(field, doc): + def fget(self): + return getattr(self._message, field, None) + + def fset(self, value): + object.__setattr__(self._message, field, value) + + return property(fget, fset, None, doc) + + +class BotInfo: + @property + def version(self): + """ + Version number of this information, incremented whenever it changes. + """ + return self._user.bot_info_version + + @property + def chat_history_access(self): + """ + `True` if the bot has privacy mode disabled via @BotFather and can see *all* messages of the group. + """ + return self._user.bot_chat_history + + @property + def private_only(self): + """ + `True` if the bot cannot be added to group and can only be used in private messages. + """ + return self._user.bot_nochats + + @property + def inline_geo(self): + """ + `True` if the bot can request the user's geolocation when used in @bot inline mode. + """ + return self._user.bot_inline_geo + + @property + def inline_placeholder(self): + """ + The placeholder to show when using the @bot inline mode. + """ + return self._user.bot_inline_placeholder + + def __init__(self, user): + self._user = user + + +class User: + """ + Represents a :tl:`User` (or :tl:`UserEmpty`, or :tl:`UserFull`) from the API. + """ + + id = _fwd('id', """ + The user identifier. This is the only property which will **always** be present. + """) + + first_name = _fwd('first_name', """ + The user's first name. It will be ``None`` for deleted accounts. + """) + + last_name = _fwd('last_name', """ + The user's last name. It can be ``None``. + """) + + username = _fwd('username', """ + The user's @username. It can be ``None``. + """) + + phone = _fwd('phone', """ + The user's phone number. It can be ``None`` if the user is not in your contacts or their + privacy setting does not allow you to view the phone number. + """) + + is_self = _fwd('is_self', """ + ``True`` if this user represents the logged-in account. + """) + + bot = _fwd('bot', """ + ``True`` if this user is a bot created via @BotFather. + """) + + contact = _fwd('contact', """ + ``True`` if this user is in the contact list of the logged-in account. + """) + + mutual_contact = _fwd('mutual_contact', """ + ``True`` if this user is in the contact list of the logged-in account, + and the user also has the logged-in account in their contact list. + """) + + deleted = _fwd('deleted', """ + ``True`` if this user belongs to a deleted account. + """) + + verified = _fwd('verified', """ + ``True`` if this user represents an official account verified by Telegram. + """) + + restricted = _fwd('restricted', """ + `True` if the user has been restricted for some reason. + """) + + support = _fwd('support', """ + ``True`` if this user belongs to an official account from Telegram Support. + """) + + scam = _fwd('scam', """ + ``True`` if this user has been flagged as spam. + """) + + fake = _fwd('fake', """ + ``True`` if this user has been flagged as fake. + """) + + lang_code = _fwd('lang_code', """ + Language code of the user, if it's known. + """) + + @property + def restriction_reasons(self): + """ + Returns a possibly-empty list of reasons why the chat is restricted to some platforms. + """ + try: + return self._user.restriction_reason or [] + except AttributeError: + return [] + + @property + def bot_info(self): + """ + Additional information about the user if it's a bot, `None` otherwise. + """ + return BotInfo(self._user) if self.bot else None + + def __init__(self): + raise TypeError('You cannot create User instances by hand!') + + @classmethod + def _new(cls, client, user): + self = cls.__new__(cls) + + self._client = client + self._user = user + self._full = None + raise RuntimeError('self._i_need_to_include_participant_info') + + return self + + async def fetch(self, *, full=False): + """ + Perform an API call to fetch fresh information about this user. + + Returns itself, but with the information fetched (allowing you to chain the call). + + If ``full`` is ``True``, the full information about the user will be fetched, + which will include things like ``about``. + """ + + # sender - might just be hash + # get sender - might be min + # sender fetch - never min + + return self + + def compact(self): + """ + Return a compact representation of this user, useful for storing for later use. + """ + raise RuntimeError('TODO') + + @property + def client(self): + """ + Returns the `TelegramClient ` + which returned this user from a friendly method. + """ + return self._client + + def to_dict(self): + return self._user.to_dict() + + def __repr__(self): + return helpers.pretty_print(self) + + def __str__(self): + return helpers.pretty_print(self, max_depth=2) + + def stringify(self): + return helpers.pretty_print(self, indent=0) + + def download_profile_photo(): + # why'd you want to access photo? just do this + pass + + def get_profile_photos(): + # this i can understand as you can pick other photos... sadly exposing raw api + pass + + # TODO status, photo, and full properties + + @property + def is_user(self): + """ + Returns `True`. + + This property also exists in `Chat`, where it returns `False`. + + .. code-block:: python + + if message.sender.is_user: + ... # do stuff + """ + return True + + @property + def is_group(self): + """ + Returns `False`. + + This property also exists in `Chat`, where it can return `True`. + + .. code-block:: python + + if message.sender.is_group: + ... # do stuff + """ + return False + + @property + def is_broadcast(self): + """ + Returns `False`. + + This property also exists in `Chat`, where it can return `True`. + + .. code-block:: python + + if message.sender.is_broadcast: + ... # do stuff + """ + return False + + @property + def full_name(self): + """ + Returns the user's full name (first name and last name concatenated). + + This property also exists in `Chat`, where it returns the title. + + .. code-block:: python + + print(message.sender.full_name): + """ + return f'{self.first_name} {self.last_name}' if self.last_name else self.first_name From f0654a08334a7cc154076e3eef0d1dac08566a7e Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 12 Feb 2022 11:30:16 +0100 Subject: [PATCH 216/256] Change Chat and Sender Getter interface to match new description --- telethon/types/_custom/chatgetter.py | 147 +++++++------------------ telethon/types/_custom/sendergetter.py | 98 +++++++---------- 2 files changed, 74 insertions(+), 171 deletions(-) diff --git a/telethon/types/_custom/chatgetter.py b/telethon/types/_custom/chatgetter.py index 03306849..3f991d7f 100644 --- a/telethon/types/_custom/chatgetter.py +++ b/telethon/types/_custom/chatgetter.py @@ -6,138 +6,65 @@ from ... import errors, _tl class ChatGetter(abc.ABC): """ - Helper base class that introduces the `chat`, `input_chat` - and `chat_id` properties and `get_chat` and `get_input_chat` - methods. + Helper base class that introduces the chat-related properties and methods. """ - def __init__(self, chat_peer=None, *, input_chat=None, chat=None, broadcast=None): - self._chat_peer = chat_peer - self._input_chat = input_chat + def __init__(self, chat, client): self._chat = chat - self._broadcast = broadcast - self._client = None + self._client = client @property def chat(self): """ - Returns the :tl:`User`, :tl:`Chat` or :tl:`Channel` where this object - belongs to. It may be `None` if Telegram didn't send the chat. + Returns the `User` or `Chat` who sent this object, or `None` if there is no chat. - If you only need the ID, use `chat_id` instead. + The chat of an event is only guaranteed to include the ``id``. + If you need the chat to at least have basic information, use `get_chat` instead. - If you need to call a method which needs - this chat, use `input_chat` instead. - - If you're using `telethon.events`, use `get_chat()` instead. + Chats obtained through friendly methods (not events) will always have complete + information (so there is no need to use `get_chat` or ``chat.fetch()``). """ return self._chat async def get_chat(self): """ - Returns `chat`, but will make an API call to find the - chat unless it's already cached. + Returns `chat`, but will make an API call to find the chat unless it's already cached. If you only need the ID, use `chat_id` instead. - If you need to call a method which needs - this chat, use `get_input_chat()` instead. - """ - # See `get_sender` for information about 'min'. - if (self._chat is None or getattr(self._chat, 'min', None))\ - and await self.get_input_chat(): - try: - self._chat =\ - await self._client.get_entity(self._input_chat) - except ValueError: - await self._refetch_chat() - return self._chat + If you need to call a method which needs this chat, prefer `chat` instead. - @property - def input_chat(self): - """ - This :tl:`InputPeer` is the input version of the chat where the - message was sent. Similarly to `input_sender - `, this - doesn't have things like username or similar, but still useful in - some cases. + Telegram may send a "minimal" version of the chat to save on bandwidth when using events. + If you need all the information about the chat upfront, you can use ``chat.fetch()``. - Note that this might not be available if the library doesn't - have enough information available. - """ - return self._input_chat + .. code-block:: python - async def get_input_chat(self): - """ - Returns `input_chat`, but will make an API call to find the - input chat unless it's already cached. - """ - if self.input_chat is None and self.chat_id and self._client: - try: - # The chat may be recent, look in dialogs - target = self.chat_id - async for d in self._client.get_dialogs(100): - if d.id == target: - self._chat = d.entity - self._input_chat = d.input_entity - break - except errors.RpcError: - pass + @client.on(events.NewMessage) + async def handler(event): + # I only need the ID -> use chat_id + chat_id = event.chat_id - return self._input_chat + # I'm going to use the chat in a method -> use chat + await client.send_message(event.chat, 'Hi!') + + # I need the chat's title -> use get_chat + chat = await event.get_chat() + print(chat.title) + + # I want to see all the information about the chat -> use fetch + chat = await event.chat.fetch() + print(chat.stringify()) + + # ... + + async for message in client.get_messages(chat): + # Here there's no need to fetch the chat - get_messages already did + print(message.chat.stringify()) + """ + raise RuntimeError('TODO') @property def chat_id(self): """ - Returns the marked chat integer ID. Note that this value **will - be different** from ``peer_id`` for incoming private messages, since - the chat *to* which the messages go is to your own person, but - the *chat* itself is with the one who sent the message. - - TL;DR; this gets the ID that you expect. - - If there is a chat in the object, `chat_id` will *always* be set, - which is why you should use it instead of `chat.id `. - """ - return utils.get_peer_id(self._chat_peer) if self._chat_peer else None - - @property - def is_private(self): - """ - `True` if the message was sent as a private message. - - Returns `None` if there isn't enough information - (e.g. on `events.MessageDeleted `). - """ - return isinstance(self._chat_peer, _tl.PeerUser) if self._chat_peer else None - - @property - def is_group(self): - """ - True if the message was sent on a group or megagroup. - - Returns `None` if there isn't enough information - (e.g. on `events.MessageDeleted `). - """ - # TODO Cache could tell us more in the future - if self._broadcast is None and hasattr(self.chat, 'broadcast'): - self._broadcast = bool(self.chat.broadcast) - - if isinstance(self._chat_peer, _tl.PeerChannel): - if self._broadcast is None: - return None - else: - return not self._broadcast - - return isinstance(self._chat_peer, _tl.PeerChat) - - @property - def is_channel(self): - """`True` if the message was sent on a megagroup or channel.""" - # The only case where chat peer could be none is in MessageDeleted, - # however those always have the peer in channels. - return isinstance(self._chat_peer, _tl.PeerChannel) - - async def _refetch_chat(self): - """ - Re-fetches chat information through other means. + Alias for ``self.chat.id``, but checking if ``chat is not None`` first. """ + return self._chat.id if self._chat else None diff --git a/telethon/types/_custom/sendergetter.py b/telethon/types/_custom/sendergetter.py index 58d84657..9f264cc0 100644 --- a/telethon/types/_custom/sendergetter.py +++ b/telethon/types/_custom/sendergetter.py @@ -3,89 +3,65 @@ import abc class SenderGetter(abc.ABC): """ - Helper base class that introduces the `sender`, `input_sender` - and `sender_id` properties and `get_sender` and `get_input_sender` - methods. + Helper base class that introduces the sender-related properties and methods. """ - def __init__(self, sender_id=None, *, sender=None, input_sender=None): - self._sender_id = sender_id + def __init__(self, sender, client): self._sender = sender - self._input_sender = input_sender - self._client = None + self._client = client @property def sender(self): """ - Returns the :tl:`User` or :tl:`Channel` that sent this object. - It may be `None` if Telegram didn't send the sender. + Returns the `User` or `Chat` who sent this object, or `None` if there is no sender. - If you only need the ID, use `sender_id` instead. + The sender of an event is only guaranteed to include the ``id``. + If you need the sender to at least have basic information, use `get_sender` instead. - If you need to call a method which needs - this chat, use `input_sender` instead. - - If you're using `telethon.events`, use `get_sender()` instead. + Senders obtained through friendly methods (not events) will always have complete + information (so there is no need to use `get_sender` or ``sender.fetch()``). """ return self._sender async def get_sender(self): """ - Returns `sender`, but will make an API call to find the - sender unless it's already cached. + Returns `sender`, but will make an API call to find the sender unless it's already cached. If you only need the ID, use `sender_id` instead. - If you need to call a method which needs - this sender, use `get_input_sender()` instead. - """ - # ``sender.min`` is present both in :tl:`User` and :tl:`Channel`. - # It's a flag that will be set if only minimal information is - # available (such as display name, but username may be missing), - # in which case we want to force fetch the entire thing because - # the user explicitly called a method. If the user is okay with - # cached information, they may use the property instead. - if (self._sender is None or getattr(self._sender, 'min', None)) \ - and await self.get_input_sender(): - try: - self._sender =\ - await self._client.get_entity(self._input_sender) - except ValueError: - await self._refetch_sender() - return self._sender + If you need to call a method which needs this sender, prefer `sender` instead. - @property - def input_sender(self): - """ - This :tl:`InputPeer` is the input version of the user/channel who - sent the message. Similarly to `input_chat - `, this doesn't - have things like username or similar, but still useful in some cases. + Telegram may send a "minimal" version of the sender to save on bandwidth when using events. + If you need all the information about the sender upfront, you can use ``sender.fetch()``. - Note that this might not be available if the library can't - find the input chat, or if the message a broadcast on a channel. - """ - return self._input_sender + .. code-block:: python - async def get_input_sender(self): + @client.on(events.NewMessage) + async def handler(event): + # I only need the ID -> use sender_id + sender_id = event.sender_id + + # I'm going to use the sender in a method -> use sender + await client.send_message(event.sender, 'Hi!') + + # I need the sender's first name -> use get_sender + sender = await event.get_sender() + print(sender.first_name) + + # I want to see all the information about the sender -> use fetch + sender = await event.sender.fetch() + print(sender.stringify()) + + # ... + + async for message in client.get_messages(chat): + # Here there's no need to fetch the sender - get_messages already did + print(message.sender.stringify()) """ - Returns `input_sender`, but will make an API call to find the - input sender unless it's already cached. - """ - if self.input_sender is None and self._sender_id and self._client: - await self._refetch_sender() - return self._input_sender + raise RuntimeError('TODO') @property def sender_id(self): """ - Returns the marked sender integer ID, if present. - - If there is a sender in the object, `sender_id` will *always* be set, - which is why you should use it instead of `sender.id `. - """ - return self._sender_id - - async def _refetch_sender(self): - """ - Re-fetches sender information through other means. + Alias for ``self.sender.id``, but checking if ``sender is not None`` first. """ + return self._sender.id if sender else None From c914a92dcfca3a98d02dbe591df49a8cc36bcc43 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 12 Feb 2022 11:30:29 +0100 Subject: [PATCH 217/256] Start using the new Sender and Chat Getter interface --- telethon/_events/callbackquery.py | 17 +---------------- telethon/_events/userupdate.py | 4 ---- telethon/_sessions/types.py | 2 +- telethon/types/_custom/message.py | 12 ++++-------- .../interactive_telegram_client.py | 2 +- telethon_examples/replier.py | 2 +- 6 files changed, 8 insertions(+), 31 deletions(-) diff --git a/telethon/_events/callbackquery.py b/telethon/_events/callbackquery.py index 8d298349..585fba51 100644 --- a/telethon/_events/callbackquery.py +++ b/telethon/_events/callbackquery.py @@ -131,27 +131,12 @@ class CallbackQuery(EventBuilder, _custom.chatgetter.ChatGetter, _custom.senderg return self._message try: - chat = await self.get_input_chat() if self.is_channel else None - self._message = await self._client.get_messages( - chat, ids=self._message_id) + self._message = await self._client.get_messages(self.chat, ids=self._message_id) except ValueError: return return self._message - async def _refetch_sender(self): - self._sender = self._entities.get(self.sender_id) - if not self._sender: - return - - self._input_sender = utils.get_input_peer(self._chat) - if not getattr(self._input_sender, 'access_hash', True): - # getattr with True to handle the InputPeerSelf() case - m = await self.get_message() - if m: - self._sender = m._sender - self._input_sender = m._input_sender - async def answer( self, message=None, cache_time=0, *, url=None, alert=False): """ diff --git a/telethon/_events/userupdate.py b/telethon/_events/userupdate.py index b9af86e7..24d87708 100644 --- a/telethon/_events/userupdate.py +++ b/telethon/_events/userupdate.py @@ -104,10 +104,6 @@ class UserUpdate(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergett """Alias for `input_sender `.""" return self.input_sender - async def get_input_user(self): - """Alias for `get_input_sender `.""" - return await self.get_input_sender() - @property def user_id(self): """Alias for `sender_id `.""" diff --git a/telethon/_sessions/types.py b/telethon/_sessions/types.py index 3aca03e4..4c9aa464 100644 --- a/telethon/_sessions/types.py +++ b/telethon/_sessions/types.py @@ -136,7 +136,7 @@ class Entity: return self.ty in (EntityType.GROUP, EntityType.MEGAGROUP) @property - def is_channel(self): + def is_broadcast(self): """ ``True`` if the entity is a broadcast channel or `broadcast group`_. diff --git a/telethon/types/_custom/message.py b/telethon/types/_custom/message.py index 84c2e409..f87142ee 100644 --- a/telethon/types/_custom/message.py +++ b/telethon/types/_custom/message.py @@ -866,7 +866,7 @@ class Message(ChatGetter, SenderGetter): """ # If the client wasn't set we can't emulate the behaviour correctly, # so as a best-effort simply return the chat peer. - if not self.out and self.is_private: + if not self.out and self.chat.is_user: return _tl.PeerUser(self._client._session_state.user_id) return self.peer_id @@ -928,7 +928,7 @@ class Message(ChatGetter, SenderGetter): # Bots cannot access other bots' messages by their ID. # However they can access them through replies... self._reply_message = await self._client.get_messages( - await self.get_input_chat() if self.is_channel else None, + self.chat, ids=_tl.InputMessageReplyTo(self.id) ) if not self._reply_message: @@ -937,7 +937,7 @@ class Message(ChatGetter, SenderGetter): # If that's the case, give it a second chance accessing # directly by its ID. self._reply_message = await self._client.get_messages( - self._input_chat if self.is_channel else None, + self.chat, ids=self.reply_to.reply_to_msg_id ) @@ -1286,8 +1286,7 @@ class Message(ChatGetter, SenderGetter): along with their input versions. """ try: - chat = await self.get_input_chat() if self.is_channel else None - msg = await self._client.get_messages(chat, ids=self.id) + msg = await self._client.get_messages(self.chat, ids=self.id) except ValueError: return # We may not have the input chat/get message failed if not msg: @@ -1302,9 +1301,6 @@ class Message(ChatGetter, SenderGetter): self._forward = msg._forward self._action_entities = msg._action_entities - async def _refetch_sender(self): - await self._reload_message() - def _set_buttons(self, chat, bot): """ Helper methods to set the buttons given the input sender and chat. diff --git a/telethon_examples/interactive_telegram_client.py b/telethon_examples/interactive_telegram_client.py index 10ca71a1..293890f5 100644 --- a/telethon_examples/interactive_telegram_client.py +++ b/telethon_examples/interactive_telegram_client.py @@ -371,7 +371,7 @@ class InteractiveTelegramClient(TelegramClient): # with events. Since they are methods, you know they may make an API # call, which can be expensive. chat = await event.get_chat() - if event.is_group: + if chat.is_group: if event.out: sprint('>> sent "{}" to chat {}'.format( event.text, get_display_name(chat) diff --git a/telethon_examples/replier.py b/telethon_examples/replier.py index e498bad9..59d17de1 100755 --- a/telethon_examples/replier.py +++ b/telethon_examples/replier.py @@ -78,7 +78,7 @@ async def handler(event): # and we said "save pic" in the message if event.out and event.is_reply and 'save pic' in event.raw_text: reply_msg = await event.get_reply_message() - replied_to_user = await reply_msg.get_input_sender() + replied_to_user = reply_msg.sender message = await event.reply('Downloading your profile photo...') file = await client.download_profile_photo(replied_to_user) From 483e2aadf13461f12fd9b14d8521d00430b5e400 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Tue, 15 Feb 2022 11:57:55 +0100 Subject: [PATCH 218/256] Begin updating the way updates are built --- telethon/_client/updates.py | 104 ++++++++------ telethon/_events/album.py | 8 +- telethon/_events/base.py | 9 +- telethon/_events/callbackquery.py | 37 ++--- telethon/_events/chataction.py | 186 +++++++++++++------------ telethon/_events/inlinequery.py | 24 ++-- telethon/_events/messagedeleted.py | 26 ++-- telethon/_events/messageedited.py | 2 +- telethon/_events/messageread.py | 39 ++++-- telethon/_events/newmessage.py | 10 +- telethon/_events/raw.py | 2 +- telethon/_events/userupdate.py | 50 +++---- telethon/_misc/utils.py | 36 +---- telethon/types/_custom/chat.py | 46 ++++-- telethon/types/_custom/chatgetter.py | 8 +- telethon/types/_custom/message.py | 6 +- telethon/types/_custom/sendergetter.py | 8 +- telethon/types/_custom/user.py | 20 ++- 18 files changed, 337 insertions(+), 284 deletions(-) diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index 16c20266..32375dd6 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -170,7 +170,7 @@ async def _update_loop(self: 'TelegramClient'): updates_to_dispatch = deque() while self.is_connected(): if updates_to_dispatch: - await _dispatch(self, updates_to_dispatch.popleft()) + await _dispatch(self, *updates_to_dispatch.popleft()) continue get_diff = self._message_box.get_difference() @@ -178,8 +178,7 @@ async def _update_loop(self: 'TelegramClient'): self._log[__name__].info('Getting difference for account updates') diff = await self(get_diff) updates, users, chats = self._message_box.apply_difference(diff, self._entity_cache) - self._entity_cache.extend(users, chats) - updates_to_dispatch.extend(updates) + updates_to_dispatch.extend(_preprocess_updates(self, updates, users, chats)) continue get_diff = self._message_box.get_channel_difference(self._entity_cache) @@ -187,8 +186,7 @@ async def _update_loop(self: 'TelegramClient'): self._log[__name__].info('Getting difference for channel updates') diff = await self(get_diff) updates, users, chats = self._message_box.apply_channel_difference(get_diff, diff, self._entity_cache) - self._entity_cache.extend(users, chats) - updates_to_dispatch.extend(updates) + updates_to_dispatch.extend(_preprocess_updates(self, updates, users, chats)) continue deadline = self._message_box.check_deadlines() @@ -203,46 +201,74 @@ async def _update_loop(self: 'TelegramClient'): processed = [] users, chats = self._message_box.process_updates(updates, self._entity_cache, processed) - self._entity_cache.extend(users, chats) - updates_to_dispatch.extend(processed) + updates_to_dispatch.extend(_preprocess_updates(self, processed, users, chats)) except Exception: self._log[__name__].exception('Fatal error handling updates (this is a bug in Telethon, please report it)') -async def _dispatch(self, update): +def _preprocess_updates(self, updates, users, chats): + self._entity_cache.extend(users, chats) + entities = Entities(self, users, chats) + return ((u, entities) for u in updates) + + +class Entities: + def __init__(self, client, users, chats): + self.self_id = client._session_state.user_id + self._entities = {e.id: e for e in itertools.chain( + (User(client, u) for u in users), + (Chat(client, c) for u in chats), + )} + + def get(self, client, peer): + if not peer: + return None + + id = utils.get_peer_id(peer) + try: + return self._entities[id] + except KeyError: + entity = client._entity_cache.get(query.user_id) + if not entity: + raise RuntimeError('Update is missing a hash but did not trigger a gap') + + self._entities[entity.id] = User(client, entity) if entity.is_user else Chat(client, entity) + return self._entities[entity.id] + + +async def _dispatch(self, update, entities): self._dispatching_update_handlers = True + try: + event_cache = {} + for handler in self._update_handlers: + event, entities = event_cache.get(handler._event) + if not event: + # build can fail if we're missing an access hash; we want this to crash + event_cache[handler._event] = event = handler._event._build(self, update, entities) - event_cache = {} - for handler in self._update_handlers: - event = event_cache.get(handler._event) - if not event: - event_cache[handler._event] = event = handler._event._build( - update, [], self._session_state.user_id, {}, self) - - while True: - # filters can be modified at any time, and there can be any amount of them which are not yet resolved - try: - if handler._filter(event): - try: - await handler._callback(event) - except StopPropagation: - self._dispatching_update_handlers = False - return - except Exception: - name = getattr(handler._callback, '__name__', repr(handler._callback)) - self._log[__name__].exception('Unhandled exception on %s (this is likely a bug in your code)', name) - except NotResolved as nr: + while True: + # filters can be modified at any time, and there can be any amount of them which are not yet resolved try: - await nr.unresolved.resolve() - continue + if handler._filter(event): + try: + await handler._callback(event) + except StopPropagation: + return + except Exception: + name = getattr(handler._callback, '__name__', repr(handler._callback)) + self._log[__name__].exception('Unhandled exception on %s (this is likely a bug in your code)', name) + except NotResolved as nr: + try: + await nr.unresolved.resolve() + continue + except Exception as e: + # we cannot really do much about this; it might be a temporary network issue + warnings.warn(f'failed to resolve filter, handler will be skipped: {e}: {nr.unresolved!r}') except Exception as e: - # we cannot really do much about this; it might be a temporary network issue - warnings.warn(f'failed to resolve filter, handler will be skipped: {e}: {nr.unresolved!r}') - except Exception as e: - # invalid filter (e.g. types when types were not used as input) - warnings.warn(f'invalid filter applied, handler will be skipped: {e}: {e.filter!r}') + # invalid filter (e.g. types when types were not used as input) + warnings.warn(f'invalid filter applied, handler will be skipped: {e}: {e.filter!r}') - # we only want to continue on unresolved filter (to check if there are more unresolved) - break - - self._dispatching_update_handlers = False + # we only want to continue on unresolved filter (to check if there are more unresolved) + break + finally: + self._dispatching_update_handlers = False diff --git a/telethon/_events/album.py b/telethon/_events/album.py index 41646acf..d57968f0 100644 --- a/telethon/_events/album.py +++ b/telethon/_events/album.py @@ -107,7 +107,7 @@ class Album(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.Se _custom.sendergetter.SenderGetter.__init__(self, message.sender_id) self.messages = messages - def _build(cls, update, others=None, self_id=None, *todo, **todo2): + def _build(cls, client, update, entities): if not others: return # We only care about albums which come inside the same Updates @@ -146,6 +146,12 @@ class Album(EventBuilder, _custom.chatgetter.ChatGetter, _custom.sendergetter.Se and u.message.grouped_id == group) ]) + self = cls.__new__(cls) + self._client = client + self._sender = entities.get(_tl.PeerUser(update.user_id)) + self._chat = entities.get(_tl.PeerUser(update.user_id)) + return self + def _set_client(self, client): super()._set_client(client) self._sender, self._input_sender = utils._get_entity_pair(self.sender_id, self._entities) diff --git a/telethon/_events/base.py b/telethon/_events/base.py index f0af195f..404ff7b6 100644 --- a/telethon/_events/base.py +++ b/telethon/_events/base.py @@ -34,15 +34,12 @@ class StopPropagation(Exception): class EventBuilder(abc.ABC): @classmethod @abc.abstractmethod - def _build(cls, update, others, self_id, entities, client): + def _build(cls, client, update, entities): """ Builds an event for the given update if possible, or returns None. - `others` are the rest of updates that came in the same container - as the current `update`. - - `self_id` should be the current user's ID, since it is required - for some events which lack this information but still need it. + `entities` must have `get(Peer) -> User|Chat` and `self_id`, + which must be the current user's ID. """ diff --git a/telethon/_events/callbackquery.py b/telethon/_events/callbackquery.py index 585fba51..5d36856b 100644 --- a/telethon/_events/callbackquery.py +++ b/telethon/_events/callbackquery.py @@ -69,29 +69,32 @@ class CallbackQuery(EventBuilder, _custom.chatgetter.ChatGetter, _custom.senderg Button.inline('Nope', b'no') ]) """ - def __init__(self, query, peer, msg_id): - _custom.chatgetter.ChatGetter.__init__(self, peer) - _custom.sendergetter.SenderGetter.__init__(self, query.user_id) + @classmethod + def _build(cls, client, update, entities): + query = update + if isinstance(update, _tl.UpdateBotCallbackQuery): + peer = update.peer + msg_id = update.msg_id + elif isinstance(update, _tl.UpdateInlineBotCallbackQuery): + # See https://github.com/LonamiWebs/Telethon/pull/1005 + # The long message ID is actually just msg_id + peer_id + msg_id, pid = struct.unpack(' Date: Wed, 16 Feb 2022 11:24:20 +0100 Subject: [PATCH 219/256] Make is_connected a property This is consistent with the rest of is_ properties --- readthedocs/misc/v2-migration-guide.rst | 2 ++ telethon/_client/auth.py | 2 +- telethon/_client/telegrambaseclient.py | 41 ++++++++++++------------- telethon/_client/telegramclient.py | 6 ++-- telethon/_client/updates.py | 2 +- telethon_examples/gui.py | 2 +- 6 files changed, 29 insertions(+), 26 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 09618508..12ae66ba 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -976,3 +976,5 @@ Now the URL is returned. You can still use ``webbrowser.open`` to get the old be todo update send_message and send_file docs (well review all functions) album overhaul. use a list of Message instead. + +is_connected is now a property (consistent with the rest of ``is_`` properties) diff --git a/telethon/_client/auth.py b/telethon/_client/auth.py index a4942a64..67e6382e 100644 --- a/telethon/_client/auth.py +++ b/telethon/_client/auth.py @@ -71,7 +71,7 @@ def start( async def _start( self: 'TelegramClient', phone, password, bot_token, code_callback, first_name, last_name, max_attempts): - if not self.is_connected(): + if not self.is_connected: await self.connect() # Rather than using `is_user_authorized`, use `get_me`. While this is diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index 09b1db51..aaaa9a7f 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -308,12 +308,29 @@ async def connect(self: 'TelegramClient') -> None: self._updates_handle = asyncio.create_task(self._update_loop()) + def is_connected(self: 'TelegramClient') -> bool: - sender = getattr(self, '_sender', None) - return sender and sender.is_connected() + return self._sender.is_connected() + async def disconnect(self: 'TelegramClient'): - return await _disconnect_coro(self) + await _disconnect(self) + + # Also clean-up all exported senders because we're done with them + async with self._borrow_sender_lock: + for state, sender in self._borrowed_senders.values(): + # Note that we're not checking for `state.should_disconnect()`. + # If the user wants to disconnect the client, ALL connections + # to Telegram (including exported senders) should be closed. + # + # Disconnect should never raise, so there's no try/except. + await sender.disconnect() + # Can't use `mark_disconnected` because it may be borrowed. + state._connected = False + + # If any was borrowed + self._borrowed_senders.clear() + def set_proxy(self: 'TelegramClient', proxy: typing.Union[tuple, dict]): init_proxy = None @@ -333,24 +350,6 @@ def set_proxy(self: 'TelegramClient', proxy: typing.Union[tuple, dict]): else: connection._proxy = proxy -async def _disconnect_coro(self: 'TelegramClient'): - await _disconnect(self) - - # Also clean-up all exported senders because we're done with them - async with self._borrow_sender_lock: - for state, sender in self._borrowed_senders.values(): - # Note that we're not checking for `state.should_disconnect()`. - # If the user wants to disconnect the client, ALL connections - # to Telegram (including exported senders) should be closed. - # - # Disconnect should never raise, so there's no try/except. - await sender.disconnect() - # Can't use `mark_disconnected` because it may be borrowed. - state._connected = False - - # If any was borrowed - self._borrowed_senders.clear() - async def _disconnect(self: 'TelegramClient'): """ diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 23babf83..eca8ba84 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -2637,7 +2637,7 @@ class TelegramClient: print('Failed to connect') """ - @forward_call(telegrambaseclient.is_connected) + @property def is_connected(self: 'TelegramClient') -> bool: """ Returns `True` if the user has connected. @@ -2647,9 +2647,11 @@ class TelegramClient: Example .. code-block:: python - while client.is_connected(): + # This is a silly example - run_until_disconnected is often better suited + while client.is_connected: await asyncio.sleep(1) """ + return telegrambaseclient.is_connected(self) @forward_call(telegrambaseclient.disconnect) def disconnect(self: 'TelegramClient'): diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index 32375dd6..91e0b05e 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -168,7 +168,7 @@ async def catch_up(self: 'TelegramClient'): async def _update_loop(self: 'TelegramClient'): try: updates_to_dispatch = deque() - while self.is_connected(): + while self.is_connected: if updates_to_dispatch: await _dispatch(self, *updates_to_dispatch.popleft()) continue diff --git a/telethon_examples/gui.py b/telethon_examples/gui.py index ea6d248a..78dcfb34 100644 --- a/telethon_examples/gui.py +++ b/telethon_examples/gui.py @@ -228,7 +228,7 @@ class App(tkinter.Tk): """ Sends a message. Does nothing if the client is not connected. """ - if not self.cl.is_connected(): + if not self.cl.is_connected: return # The user needs to configure a chat where the message should be sent. From df0e710fa1c18515dbb2126ffb181a13bab17cce Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 16 Feb 2022 12:23:19 +0100 Subject: [PATCH 220/256] Add a custom SentCode type --- telethon/_client/auth.py | 4 +- telethon/_client/telegramclient.py | 16 +++- telethon/types/__init__.py | 2 + telethon/types/_custom/__init__.py | 1 + telethon/types/_custom/auth.py | 120 +++++++++++++++++++++++++++++ 5 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 telethon/types/_custom/auth.py diff --git a/telethon/_client/auth.py b/telethon/_client/auth.py index 67e6382e..a82aec03 100644 --- a/telethon/_client/auth.py +++ b/telethon/_client/auth.py @@ -335,7 +335,7 @@ async def _replace_session_state(self, *, save=True, **changes): async def send_code_request( self: 'TelegramClient', - phone: str) -> '_tl.auth.SentCode': + phone: str) -> 'SentCode': result = None phone = utils.parse_phone(phone) or self._phone phone_hash = self._phone_code_hash.get(phone) @@ -358,7 +358,7 @@ async def send_code_request( self._phone = phone - return result + return _custom.SentCode._new(result) async def qr_login(self: 'TelegramClient', ignored_ids: typing.List[int] = None) -> _custom.QRLogin: qr_login = _custom.QRLogin(self, ignored_ids or []) diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index eca8ba84..5ac3fa4d 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -504,7 +504,7 @@ class TelegramClient: @forward_call(auth.send_code_request) async def send_code_request( self: 'TelegramClient', - phone: str) -> '_tl.auth.SentCode': + phone: str) -> 'SentCode': """ Sends the Telegram code needed to login to the given phone number. @@ -513,14 +513,24 @@ class TelegramClient: The phone to which the code will be sent. Returns - An instance of :tl:`SentCode`. + An instance of `SentCode`. Example .. code-block:: python phone = '+34 123 123 123' sent = await client.send_code_request(phone) - print(sent) + print(sent.type) + + # Wait before resending sent.next_type, if any + if sent.next_type: + await asyncio.sleep(sent.timeout or 0) + resent = await client.send_code_request(phone) + print(sent.type) + + # Checking the code locally + code = input('Enter code: ') + print('Code looks OK:', resent.check(code)) """ @forward_call(auth.qr_login) diff --git a/telethon/types/__init__.py b/telethon/types/__init__.py index ac52ff6d..2fa8cd02 100644 --- a/telethon/types/__init__.py +++ b/telethon/types/__init__.py @@ -1,5 +1,7 @@ from .._misc.tlobject import TLObject, TLRequest from ._custom import ( + CodeType, + SentCode, AdminLogEvent, Draft, Dialog, diff --git a/telethon/types/_custom/__init__.py b/telethon/types/_custom/__init__.py index 00a0d00f..5637c1e2 100644 --- a/telethon/types/_custom/__init__.py +++ b/telethon/types/_custom/__init__.py @@ -1,4 +1,5 @@ from .adminlogevent import AdminLogEvent +from .auth import CodeType, SentCode from .draft import Draft from .dialog import Dialog from .inputsizedfile import InputSizedFile diff --git a/telethon/types/_custom/auth.py b/telethon/types/_custom/auth.py new file mode 100644 index 00000000..bfd1ad09 --- /dev/null +++ b/telethon/types/_custom/auth.py @@ -0,0 +1,120 @@ +import asyncio +import re +from enum import Enum, auto +from ... import _tl + + +class CodeType(Enum): + """ + The type of the login code sent. + + When resending the code, it won't be APP a second time. + """ + + APP = auto() + SMS = auto() + CALL = auto() + FLASH_CALL = auto() + MISSED_CALL = auto() + + +class SentCode: + """ + Information about the login code request, returned by `client.send_code_request`. + """ + + @classmethod + def _new(cls, code): + self = cls.__new__(cls) + self._code = code + self._start = asyncio.get_running_loop().time() + return self + + @property + def type(self): + """ + The `CodeType` which was sent. + """ + return { + _tl.auth.SentCodeTypeApp: CodeType.APP, + _tl.auth.SentCodeTypeSms: CodeType.SMS, + _tl.auth.SentCodeTypeCall: CodeType.CALL, + _tl.auth.SentCodeTypeFlashCall: CodeType.FLASH_CALL, + _tl.auth.SentCodeTypeMissedCall: CodeType.MISSED_CALL, + }[type(self._code.type)] + + @property + def next_type(self): + """ + The `CodeType` which will be sent if `client.send_code_request` + is used again after `timeout` seconds have elapsed. It may be `None`. + """ + if not self._code.next_type: + return None + + return { + _tl.auth.CodeTypeSms: CodeType.SMS, + _tl.auth.CodeTypeCall: CodeType.CALL, + _tl.auth.CodeTypeFlashCall: CodeType.FLASH_CALL, + _tl.auth.CodeTypeMissedCall: CodeType.MISSED_CALL, + }[type(self._code.next_type)] + + @property + def timeout(self): + """ + How many seconds are left before `client.send_code_request` can be used to resend the code. + Resending the code before this many seconds have elapsed may or may not work. + + This value can be `None`. + + This value is a positive floating point number, and is monotically decreasing. + The value will reach zero after enough seconds have elapsed. This lets you do some work + and call sleep on the value and still wait just long enough. + + If you need the original timeout, call `round` on the value as soon as possible. + """ + if not self._code.timeout: + return None + + return max(0.0, (self._start + self._code.timeout) - asyncio.get_running_loop().time()) + + @property + def length(self): + """ + The length of the sent code. + + If the length is unknown (it could be any length), `None` is returned. + This can be true for `CodeType.FLASH_CALL`. + """ + if isinstance(self._code.type, _tl.auth.SentCodeTypeFlashCall): + return None if self._code.type.pattern in ('', '*') else len(self._code.type.pattern) + else: + return self._code.type.length + + def check(self, code): + """ + Check if the user's input code is valid. + + This can be used to implement a client-side validation before actually trying to login + (mostly useful with a graphic interface, to hint the user the code is not yet correct). + """ + if not isinstance(code, str): + raise TypeError(f'code must be str, but was {type(code)}') + + if isinstance(self._code.type, _tl.auth.SentCodeTypeFlashCall): + if self._code.type.pattern in ('', '*'): + return True + + if not all(c.isdigit() or c == '*' for c in self._code.type.pattern): + # Potentially unsafe to use this pattern in a regex + raise RuntimeError(f'Unrecognised code pattern: {self._code.type.pattern!r}') + + pattern = self._code.type.pattern.replace('*', r'\d*') + numbers = ''.join(c for c in code if c.isdigit()) + return re.match(f'^{pattern}$', numbers) is not None + + if isinstance(self._code.type, _tl.auth.SentCodeTypeMissedCall): + if not code.startswith(self._code.type.prefix): + return False + + return len(code) == self._code.type.length From 90bd5de74a3461c5945317bae6cb72b7002dbbfb Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 16 Feb 2022 12:54:41 +0100 Subject: [PATCH 221/256] Remove phone and hash from sign in --- readthedocs/misc/v2-migration-guide.rst | 4 ++ telethon/_client/auth.py | 68 +++++++++---------------- telethon/_client/telegrambaseclient.py | 2 +- telethon/_client/telegramclient.py | 32 +++++------- telethon/errors/__init__.py | 1 + telethon/errors/_custom.py | 6 +++ 6 files changed, 47 insertions(+), 66 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 12ae66ba..85eb60a4 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -978,3 +978,7 @@ todo update send_message and send_file docs (well review all functions) album overhaul. use a list of Message instead. is_connected is now a property (consistent with the rest of ``is_`` properties) + +send_code_request now returns a custom type (reducing raw api). +sign_in no longer has phone or phone_hash (these are impl details, and now it's less error prone). also mandatory code=. also no longer is a no-op if already logged in. different error for sign up required. +send code / sign in now only expect a single phone. resend code with new phone is send code, not resend. diff --git a/telethon/_client/auth.py b/telethon/_client/auth.py index a82aec03..6301b9b1 100644 --- a/telethon/_client/auth.py +++ b/telethon/_client/auth.py @@ -196,43 +196,22 @@ async def _start( return self -def _parse_phone_and_hash(self, phone, phone_hash): - """ - Helper method to both parse and validate phone and its hash. - """ - phone = utils.parse_phone(phone) or self._phone - if not phone: - raise ValueError( - 'Please make sure to call send_code_request first.' - ) - - phone_hash = phone_hash or self._phone_code_hash.get(phone, None) - if not phone_hash: - raise ValueError('You also need to provide a phone_code_hash.') - - return phone, phone_hash async def sign_in( self: 'TelegramClient', - phone: str = None, - code: typing.Union[str, int] = None, *, + code: typing.Union[str, int] = None, password: str = None, - bot_token: str = None, - phone_code_hash: str = None) -> 'typing.Union[_tl.User, _tl.auth.SentCode]': - me = await self.get_me() - if me: - return me + bot_token: str = None,) -> 'typing.Union[_tl.User, _tl.auth.SentCode]': + if code: + if not self._phone_code_hash: + raise ValueError('Must call client.send_code_request before sign in') - if phone and code: - phone, phone_code_hash = \ - _parse_phone_and_hash(self, phone, phone_code_hash) + phone, phone_code_hash = self._phone_code_hash # May raise PhoneCodeEmptyError, PhoneCodeExpiredError, # PhoneCodeHashEmptyError or PhoneCodeInvalidError. - request = _tl.fn.auth.SignIn( - phone, phone_code_hash, str(code) - ) + request = _tl.fn.auth.SignIn(*self._phone_code_hash, str(code)) elif password: pwd = await self(_tl.fn.account.GetPassword()) request = _tl.fn.auth.CheckPassword( @@ -244,13 +223,13 @@ async def sign_in( api_id=self._api_id, api_hash=self._api_hash ) else: - raise ValueError('You must provide either phone and code, password, or bot_token.') + raise ValueError('You must provide code, password, or bot_token.') result = await self(request) if isinstance(result, _tl.auth.AuthorizationSignUpRequired): - # Emulate pre-layer 104 behaviour + # The method must return the User but we don't have it, so raise instead (matches pre-layer 104 behaviour) self._tos = result.terms_of_service - raise errors.PhoneNumberUnoccupiedError(request=request) + raise errors.SignUpRequired() return await _update_session_state(self, result.user) @@ -287,8 +266,10 @@ async def sign_up( sys.stderr.write("{}\n".format(self._tos.text)) sys.stderr.flush() - phone, phone_code_hash = \ - _parse_phone_and_hash(self, phone, phone_code_hash) + if not self._phone_code_hash: + raise ValueError('Must call client.send_code_request before sign up') + + phone, phone_code_hash = self._phone_code_hash result = await self(_tl.fn.auth.SignUp( phone_number=phone, @@ -321,6 +302,7 @@ async def _update_session_state(self, user, save=True): seq=state.seq, ) + self._phone_code_hash = None return user @@ -336,15 +318,10 @@ async def _replace_session_state(self, *, save=True, **changes): async def send_code_request( self: 'TelegramClient', phone: str) -> 'SentCode': - result = None - phone = utils.parse_phone(phone) or self._phone - phone_hash = self._phone_code_hash.get(phone) + phone = utils.parse_phone(phone) - if phone_hash: - result = await self( - _tl.fn.auth.ResendCode(phone, phone_hash)) - - self._phone_code_hash[phone] = result.phone_code_hash + if self._phone_code_hash and phone == self._phone_code_hash[0]: + result = await self(_tl.fn.auth.ResendCode(*self._phone_code_hash)) else: try: result = await self(_tl.fn.auth.SendCode( @@ -353,13 +330,14 @@ async def send_code_request( return await self.send_code_request(phone) # phone_code_hash may be empty, if it is, do not save it (#1283) - if result.phone_code_hash: - self._phone_code_hash[phone] = phone_hash = result.phone_code_hash - - self._phone = phone + if not result.phone_code_hash: + # The hash is required to login, so this pretty much means send code failed + raise ValueError('Failed to send code') + self._phone_code_hash = (phone, result.phone_code_hash) return _custom.SentCode._new(result) + async def qr_login(self: 'TelegramClient', ignored_ids: typing.List[int] = None) -> _custom.QRLogin: qr_login = _custom.QRLogin(self, ignored_ids or []) await qr_login.recreate() diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index aaaa9a7f..ca11d177 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -141,7 +141,7 @@ def init( self._connect_timeout = connect_timeout self.flood_sleep_threshold = flood_sleep_threshold self._flood_waited_requests = {} # prevent calls that would floodwait entirely - self._phone_code_hash = {} # used during login to prevent exposing the hash to end users + self._phone_code_hash = None # used during login to prevent exposing the hash to end users # Update handling. self._catch_up = catch_up diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 5ac3fa4d..e801b26d 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -395,12 +395,10 @@ class TelegramClient: @forward_call(auth.sign_in) async def sign_in( self: 'TelegramClient', - phone: str = None, - code: typing.Union[str, int] = None, *, + code: typing.Union[str, int] = None, password: str = None, - bot_token: str = None, - phone_code_hash: str = None) -> 'typing.Union[_tl.User, _tl.auth.SentCode]': + bot_token: str = None) -> 'typing.Union[_tl.User, _tl.auth.SentCode]': """ Logs in to Telegram to an existing user or bot account. @@ -411,16 +409,13 @@ class TelegramClient: In most cases, you should simply use `start()` and not this method. Arguments - phone (`str` | `int`): - The phone to send the code to if no code was provided, - or to override the phone that was previously used with - these requests. - code (`str` | `int`): - The code that Telegram sent. Note that if you have sent this - code through the application itself it will immediately - expire. If you want to send the code, obfuscate it somehow. - If you're not doing any of this you can ignore this note. + The code that Telegram sent. + + To login to a user account, you must use `client.send_code_request` first. + + The code will expire immediately if you send it through the application itself + as a safety measure. password (`str`): 2FA password, should be used if a previous call raised @@ -431,22 +426,19 @@ class TelegramClient: This should be the hash the `@BotFather `_ gave you. - phone_code_hash (`str`, optional): - The hash returned by `send_code_request`. This can be left as - `None` to use the last hash known for the phone to be used. + You do not need to call `client.send_code_request` to login to a bot account. Returns - The signed in user, or the information about - :meth:`send_code_request`. + The signed in `User`, if the method did not fail. Example .. code-block:: python phone = '+34 123 123 123' - await client.sign_in(phone) # send code + await client.send_code_request(phone) # send code code = input('enter code: ') - await client.sign_in(phone, code) + await client.sign_in(code=code) """ @forward_call(auth.sign_up) diff --git a/telethon/errors/__init__.py b/telethon/errors/__init__.py index 0d4ea0cc..cc83f2fe 100644 --- a/telethon/errors/__init__.py +++ b/telethon/errors/__init__.py @@ -7,6 +7,7 @@ from ._custom import ( CdnFileTamperedError, BadMessageError, MultiError, + SignUpRequired, ) from ._rpcbase import ( RpcError, diff --git a/telethon/errors/_custom.py b/telethon/errors/_custom.py index f48cdc94..b5088bbf 100644 --- a/telethon/errors/_custom.py +++ b/telethon/errors/_custom.py @@ -150,3 +150,9 @@ class MultiError(Exception): self.results = list(result) self.requests = list(requests) return self + + +class SignUpRequired(Exception): + """ + Occurs when trying to sign in with a phone number that doesn't have an account registered yet. + """ From 1029c38d7e5b8b90128c0cef31a011626c9b78bb Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 16 Feb 2022 12:59:52 +0100 Subject: [PATCH 222/256] Remove phone and hash from sign_up --- readthedocs/misc/v2-migration-guide.rst | 1 + telethon/_client/auth.py | 22 ++++++++-------------- telethon/_client/telegramclient.py | 18 ++++-------------- 3 files changed, 13 insertions(+), 28 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 85eb60a4..3380ac92 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -982,3 +982,4 @@ is_connected is now a property (consistent with the rest of ``is_`` properties) send_code_request now returns a custom type (reducing raw api). sign_in no longer has phone or phone_hash (these are impl details, and now it's less error prone). also mandatory code=. also no longer is a no-op if already logged in. different error for sign up required. send code / sign in now only expect a single phone. resend code with new phone is send code, not resend. +sign_up code is also now a kwarg. and no longer noop if already loggedin. diff --git a/telethon/_client/auth.py b/telethon/_client/auth.py index 6301b9b1..795a7f51 100644 --- a/telethon/_client/auth.py +++ b/telethon/_client/auth.py @@ -233,17 +233,17 @@ async def sign_in( return await _update_session_state(self, result.user) + async def sign_up( self: 'TelegramClient', - code: typing.Union[str, int], first_name: str, last_name: str = '', *, - phone: str = None, - phone_code_hash: str = None) -> '_tl.User': - me = await self.get_me() - if me: - return me + code: typing.Union[str, int]) -> '_tl.User': + if not self._phone_code_hash: + raise ValueError('Must call client.send_code_request before sign up') + + phone, phone_code_hash = self._phone_code_hash # To prevent abuse, one has to try to sign in before signing up. This # is the current way in which Telegram validates the code to sign up. @@ -259,18 +259,13 @@ async def sign_up( code=code, phone_code_hash=phone_code_hash, ) - except errors.PhoneNumberUnoccupiedError: + except errors.SignUpRequired: pass # code is correct and was used, now need to sign in if self._tos and self._tos.text: sys.stderr.write("{}\n".format(self._tos.text)) sys.stderr.flush() - if not self._phone_code_hash: - raise ValueError('Must call client.send_code_request before sign up') - - phone, phone_code_hash = self._phone_code_hash - result = await self(_tl.fn.auth.SignUp( phone_number=phone, phone_code_hash=phone_code_hash, @@ -279,8 +274,7 @@ async def sign_up( )) if self._tos: - await self( - _tl.fn.help.AcceptTermsOfService(self._tos.id)) + await self(_tl.fn.help.AcceptTermsOfService(self._tos.id)) return await _update_session_state(self, result.user) diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index e801b26d..df58a287 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -444,12 +444,10 @@ class TelegramClient: @forward_call(auth.sign_up) async def sign_up( self: 'TelegramClient', - code: typing.Union[str, int], first_name: str, last_name: str = '', *, - phone: str = None, - phone_code_hash: str = None) -> '_tl.User': + code: typing.Union[str, int]) -> '_tl.User': """ Signs up to Telegram as a new user account. @@ -463,22 +461,14 @@ class TelegramClient: and https://core.telegram.org/api/terms. Arguments - code (`str` | `int`): - The code sent by Telegram - first_name (`str`): The first name to be used by the new account. last_name (`str`, optional) Optional last name. - phone (`str` | `int`, optional): - The phone to sign up. This will be the last phone used by - default (you normally don't need to set this). - - phone_code_hash (`str`, optional): - The hash returned by `send_code_request`. This can be left as - `None` to use the last hash known for the phone to be used. + code (`str` | `int`): + The code sent by Telegram Returns The new created :tl:`User`. @@ -490,7 +480,7 @@ class TelegramClient: await client.send_code_request(phone) code = input('enter code: ') - await client.sign_up(code, first_name='Anna', last_name='Banana') + await client.sign_up('Anna', 'Banana', code=code) """ @forward_call(auth.send_code_request) From 5c6fdf9a71abd737e4434b7dad392c801d1673a9 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 17 Feb 2022 11:15:11 +0100 Subject: [PATCH 223/256] Return custom User from sign_in/sign_up --- telethon/_client/auth.py | 2 +- telethon/types/__init__.py | 2 ++ telethon/types/_custom/__init__.py | 2 ++ telethon/types/_custom/user.py | 3 +-- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/telethon/_client/auth.py b/telethon/_client/auth.py index 795a7f51..3c246c46 100644 --- a/telethon/_client/auth.py +++ b/telethon/_client/auth.py @@ -297,7 +297,7 @@ async def _update_session_state(self, user, save=True): ) self._phone_code_hash = None - return user + return _custom.User._new(self, user) async def _replace_session_state(self, *, save=True, **changes): diff --git a/telethon/types/__init__.py b/telethon/types/__init__.py index 2fa8cd02..665f9481 100644 --- a/telethon/types/__init__.py +++ b/telethon/types/__init__.py @@ -15,4 +15,6 @@ from ._custom import ( InlineResults, QRLogin, ParticipantPermissions, + Chat, + User, ) diff --git a/telethon/types/_custom/__init__.py b/telethon/types/_custom/__init__.py index 5637c1e2..5ee32a68 100644 --- a/telethon/types/_custom/__init__.py +++ b/telethon/types/_custom/__init__.py @@ -12,3 +12,5 @@ from .inlineresult import InlineResult from .inlineresults import InlineResults from .qrlogin import QRLogin from .participantpermissions import ParticipantPermissions +from .chat import Chat +from .user import User diff --git a/telethon/types/_custom/user.py b/telethon/types/_custom/user.py index 79e7c21e..12f2e765 100644 --- a/telethon/types/_custom/user.py +++ b/telethon/types/_custom/user.py @@ -11,7 +11,7 @@ from .inputfile import InputFile from .inputmessage import InputMessage from .button import build_reply_markup from ..._misc import utils, helpers, tlobject, markdown, html -from ... import _tl, _misc +from ..._sessions.types import Entity if TYPE_CHECKING: @@ -181,7 +181,6 @@ class User: self._user = user self._full = None - raise RuntimeError('self._i_need_to_include_participant_info') return self From 77a98fed2ca4bd162abba0c016f91b44295f4d76 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 17 Feb 2022 11:26:35 +0100 Subject: [PATCH 224/256] Make phone and password kwargs in start --- readthedocs/misc/v2-migration-guide.rst | 1 + telethon/_client/telegramclient.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 3380ac92..a43f3ffa 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -983,3 +983,4 @@ send_code_request now returns a custom type (reducing raw api). sign_in no longer has phone or phone_hash (these are impl details, and now it's less error prone). also mandatory code=. also no longer is a no-op if already logged in. different error for sign up required. send code / sign in now only expect a single phone. resend code with new phone is send code, not resend. sign_up code is also now a kwarg. and no longer noop if already loggedin. +start also mandates phone= or password= as kwarg. diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index df58a287..68393583 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -313,9 +313,9 @@ class TelegramClient: @forward_call(auth.start) def start( self: 'TelegramClient', + *, phone: typing.Callable[[], str] = lambda: input('Please enter your phone (or bot token): '), password: typing.Callable[[], str] = lambda: getpass.getpass('Please enter your password: '), - *, bot_token: str = None, code_callback: typing.Callable[[], typing.Union[str, int]] = None, first_name: str = 'New User', From 94ac667e796025527e08aa019a456a893fc1acb7 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 17 Feb 2022 11:26:53 +0100 Subject: [PATCH 225/256] Support direct sign_in with code and password --- telethon/_client/auth.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/telethon/_client/auth.py b/telethon/_client/auth.py index 3c246c46..17836d08 100644 --- a/telethon/_client/auth.py +++ b/telethon/_client/auth.py @@ -203,29 +203,36 @@ async def sign_in( code: typing.Union[str, int] = None, password: str = None, bot_token: str = None,) -> 'typing.Union[_tl.User, _tl.auth.SentCode]': + if code and bot_token: + raise ValueError('Can only provide one of code or bot_token, not both') + + if not code and not bot_token and not password: + raise ValueError('You must provide code, password, or bot_token.') + if code: if not self._phone_code_hash: raise ValueError('Must call client.send_code_request before sign in') - phone, phone_code_hash = self._phone_code_hash - # May raise PhoneCodeEmptyError, PhoneCodeExpiredError, # PhoneCodeHashEmptyError or PhoneCodeInvalidError. - request = _tl.fn.auth.SignIn(*self._phone_code_hash, str(code)) - elif password: - pwd = await self(_tl.fn.account.GetPassword()) - request = _tl.fn.auth.CheckPassword( - pwd_mod.compute_check(pwd, password) - ) + try: + result = await self(_tl.fn.auth.SignIn(*self._phone_code_hash, str(code))) + password = None # user provided a password but it was not needed + except errors.SessionPasswordNeededError: + if not password: + raise elif bot_token: - request = _tl.fn.auth.ImportBotAuthorization( + result = await self(_tl.fn.auth.ImportBotAuthorization( flags=0, bot_auth_token=bot_token, api_id=self._api_id, api_hash=self._api_hash - ) - else: - raise ValueError('You must provide code, password, or bot_token.') + )) + + if password: + pwd = await self(_tl.fn.account.GetPassword()) + result = await self(_tl.fn.auth.CheckPassword( + pwd_mod.compute_check(pwd, password) + )) - result = await self(request) if isinstance(result, _tl.auth.AuthorizationSignUpRequired): # The method must return the User but we don't have it, so raise instead (matches pre-layer 104 behaviour) self._tos = result.terms_of_service From 80d44cb75bf11a2984e6a85b3fdb8330edb71860 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 17 Feb 2022 11:30:18 +0100 Subject: [PATCH 226/256] Fix sign_up call to sign_in --- telethon/_client/auth.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/telethon/_client/auth.py b/telethon/_client/auth.py index 17836d08..bf025452 100644 --- a/telethon/_client/auth.py +++ b/telethon/_client/auth.py @@ -248,10 +248,9 @@ async def sign_up( *, code: typing.Union[str, int]) -> '_tl.User': if not self._phone_code_hash: + # This check is also present in sign_in but we do it here to customize the error message raise ValueError('Must call client.send_code_request before sign up') - phone, phone_code_hash = self._phone_code_hash - # To prevent abuse, one has to try to sign in before signing up. This # is the current way in which Telegram validates the code to sign up. # @@ -261,11 +260,7 @@ async def sign_up( # We're emulating pre-layer 104 behaviour so except the right error: if not self._tos: try: - return await self.sign_in( - phone=phone, - code=code, - phone_code_hash=phone_code_hash, - ) + return await self.sign_in(code=code) except errors.SignUpRequired: pass # code is correct and was used, now need to sign in From 0bc598c1217818e62c3b4dd9e8a05c87109714f9 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 17 Feb 2022 12:40:09 +0100 Subject: [PATCH 227/256] Stop auto-accepting ToS on sign_up, add get_tos instead --- telethon/_client/auth.py | 33 +++-- telethon/_client/telegrambaseclient.py | 1 + telethon/_client/telegramclient.py | 59 ++++++++- telethon/_client/users.py | 3 +- telethon/types/__init__.py | 1 + telethon/types/_custom/__init__.py | 1 + telethon/types/_custom/tos.py | 160 +++++++++++++++++++++++++ 7 files changed, 240 insertions(+), 18 deletions(-) create mode 100644 telethon/types/_custom/tos.py diff --git a/telethon/_client/auth.py b/telethon/_client/auth.py index bf025452..518abc8d 100644 --- a/telethon/_client/auth.py +++ b/telethon/_client/auth.py @@ -1,3 +1,4 @@ +import asyncio import getpass import inspect import os @@ -235,7 +236,7 @@ async def sign_in( if isinstance(result, _tl.auth.AuthorizationSignUpRequired): # The method must return the User but we don't have it, so raise instead (matches pre-layer 104 behaviour) - self._tos = result.terms_of_service + self._tos = (result.terms_of_service, None) raise errors.SignUpRequired() return await _update_session_state(self, result.user) @@ -258,15 +259,10 @@ async def sign_up( # because the user already tried to sign in. # # We're emulating pre-layer 104 behaviour so except the right error: - if not self._tos: - try: - return await self.sign_in(code=code) - except errors.SignUpRequired: - pass # code is correct and was used, now need to sign in - - if self._tos and self._tos.text: - sys.stderr.write("{}\n".format(self._tos.text)) - sys.stderr.flush() + try: + return await self.sign_in(code=code) + except errors.SignUpRequired: + pass # code is correct and was used, now need to sign in result = await self(_tl.fn.auth.SignUp( phone_number=phone, @@ -275,12 +271,23 @@ async def sign_up( last_name=last_name )) - if self._tos: - await self(_tl.fn.help.AcceptTermsOfService(self._tos.id)) - return await _update_session_state(self, result.user) +async def get_tos(self): + first_time = self._tos is None + no_tos = self._tos and self._tos[0] is None + tos_expired = self._tos and self._tos[1] is not None and asyncio.get_running_loop().time() >= self._tos[1] + + if first_time or no_tos or tos_expired: + result = await self(_tl.fn.help.GetTermsOfServiceUpdate()) + tos = getattr(result, 'terms_of_service', None) + self._tos = (tos, asyncio.get_running_loop().time() + result.expires) + + # not stored in the client to prevent a cycle + return _custom.TermsOfService._new(self, *self._tos) + + async def _update_session_state(self, user, save=True): """ Callback called whenever the login or sign up process completes. diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index ca11d177..ec2a0ee5 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -142,6 +142,7 @@ def init( self.flood_sleep_threshold = flood_sleep_threshold self._flood_waited_requests = {} # prevent calls that would floodwait entirely self._phone_code_hash = None # used during login to prevent exposing the hash to end users + self._tos = None # used during signup and when fetching tos (tos/expiry) # Update handling. self._catch_up = catch_up diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 68393583..c8a41c92 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -455,10 +455,15 @@ class TelegramClient: You must call `send_code_request` first. - **By using this method you're agreeing to Telegram's - Terms of Service. This is required and your account - will be banned otherwise.** See https://telegram.org/tos - and https://core.telegram.org/api/terms. + .. important:: + + When creating a new account, you must be sure to show the Terms of Service + to the user, and only after they approve, the code can accept the Terms of + Service. If not, they must be declined, in which case the account **will be + deleted**. + + Make sure to use `client.get_tos` to fetch the Terms of Service, and to + use `tos.accept()` or `tos.decline()` after the user selects an option. Arguments first_name (`str`): @@ -481,6 +486,16 @@ class TelegramClient: code = input('enter code: ') await client.sign_up('Anna', 'Banana', code=code) + + # IMPORTANT: you MUST retrieve the Terms of Service and accept + # them, or Telegram has every right to delete the account. + tos = await client.get_tos() + print(tos.html) + + if code('accept (y/n)?: ') == 'y': + await tos.accept() + else: + await tos.decline() # deletes the account! """ @forward_call(auth.send_code_request) @@ -628,6 +643,42 @@ class TelegramClient: await client.edit_2fa(current_password='I_<3_Telethon') """ + @forward_call(auth.get_tos) + async def get_tos(self: 'TelegramClient') -> '_custom.TermsOfService': + """ + Fetch `Telegram's Terms of Service`_, which every user must accept in order to use + Telegram, or they must otherwise `delete their account`_. + + This method **must** be called after sign up, and **should** be called again + after it expires (at the risk of having the account terminated otherwise). + + See the documentation of `TermsOfService` for more information. + + The library cannot automate this process because the user must read the Terms of Service. + Automating its usage without reading the terms would be done at the developer's own risk. + + Example + .. code-block:: python + + # Fetch the ToS, forever (this could be a separate task, for example) + while True: + tos = await client.get_tos() + + if tos: + # There's an update or they must be accepted (you could show a popup) + print(tos.html) + if code('accept (y/n)?: ') == 'y': + await tos.accept() + else: + await tos.decline() # deletes the account! + + # after tos.timeout expires, the method should be called again! + await asyncio.sleep(tos.timeout) + + _Telegram's Terms of Service: https://telegram.org/tos + _delete their account: https://core.telegram.org/api/config#terms-of-service + """ + async def __aenter__(self): await self.connect() return self diff --git a/telethon/_client/users.py b/telethon/_client/users.py index 4717d6b1..154a778c 100644 --- a/telethon/_client/users.py +++ b/telethon/_client/users.py @@ -10,6 +10,7 @@ from ..errors._rpcbase import RpcError, ServerError, FloodError, InvalidDcError, from .._misc import helpers, utils, hints from .._sessions.types import Entity from .. import errors, _tl +from ..types import _custom from .account import ignore_takeout _NOT_A_REQUEST = lambda: TypeError('You can only invoke requests, not types!') @@ -134,7 +135,7 @@ async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sl async def get_me(self: 'TelegramClient') \ -> 'typing.Union[_tl.User, _tl.InputPeerUser]': try: - return (await self(_tl.fn.users.GetUsers([_tl.InputUserSelf()])))[0] + return _custom.User._new(self, (await self(_tl.fn.users.GetUsers([_tl.InputUserSelf()])))[0]) except UnauthorizedError: return None diff --git a/telethon/types/__init__.py b/telethon/types/__init__.py index 665f9481..39f8520e 100644 --- a/telethon/types/__init__.py +++ b/telethon/types/__init__.py @@ -17,4 +17,5 @@ from ._custom import ( ParticipantPermissions, Chat, User, + TermsOfService, ) diff --git a/telethon/types/_custom/__init__.py b/telethon/types/_custom/__init__.py index 5ee32a68..d8945804 100644 --- a/telethon/types/_custom/__init__.py +++ b/telethon/types/_custom/__init__.py @@ -14,3 +14,4 @@ from .qrlogin import QRLogin from .participantpermissions import ParticipantPermissions from .chat import Chat from .user import User +from .tos import TermsOfService diff --git a/telethon/types/_custom/tos.py b/telethon/types/_custom/tos.py new file mode 100644 index 00000000..7b4f4c34 --- /dev/null +++ b/telethon/types/_custom/tos.py @@ -0,0 +1,160 @@ +import sys + +from typing import Optional, List, TYPE_CHECKING +from datetime import datetime +from dataclasses import dataclass +import mimetypes +from .chatgetter import ChatGetter +from .sendergetter import SenderGetter +from .messagebutton import MessageButton +from .forward import Forward +from .file import File +from .inputfile import InputFile +from .inputmessage import InputMessage +from .button import build_reply_markup +from ..._misc import utils, helpers, tlobject, markdown, html +from ... import _tl, _misc + + +_DEFAULT_TIMEOUT = 24 * 60 * 60 + + +class TermsOfService: + """ + Represents `Telegram's Terms of Service`_, which every user must accept in order to use + Telegram, or they must otherwise `delete their account`_. + + This is not the same as the `API's Terms of Service`_, which every developer must accept + before creating applications for Telegram. + + You must make sure to check for the terms text (or markdown, or HTML), as well as confirm + the user's age if required. + + This class implements `__bool__`, meaning it will be truthy if there are terms to display, + and falsey otherwise. + + .. code-block:: python + + tos = await client.get_tos() + if tos: + print(tos.html) # there's something to read and accept or decline + ... + else: + await asyncio.sleep(tos.timeout) # nothing to read, but still has tos.timeout + + _Telegram's Terms of Service: https://telegram.org/tos + _delete their account: https://core.telegram.org/api/config#terms-of-service + _API's Terms of Service: https://core.telegram.org/api/terms + """ + + @property + def text(self): + """Plain-text version of the Terms of Service, or `None` if there is no ToS update.""" + return self._tos and self._tos.text + + @property + def markdown(self): + """Markdown-formatted version of the Terms of Service, or `None` if there is no ToS update.""" + return self._tos and markdown.unparse(self._tos.text, self._tos.entities) + + @property + def html(self): + """HTML-formatted version of the Terms of Service, or `None` if there is no ToS update.""" + return self._tos and html.unparse(self._tos.text, self._tos.entities) + + @property + def popup(self): + """`True` a popup should be shown to the user.""" + return self._tos and self._tos.popup + + @property + def minimum_age(self): + """The minimum age the user must be to accept the terms, or `None` if there's no requirement.""" + return self._tos and self._tos.min_age_confirm + + @property + def timeout(self): + """ + How many seconds are left before `client.get_tos` should be used again. + + This value is a positive floating point number, and is monotically decreasing. + The value will reach zero after enough seconds have elapsed. This lets you do some work + and call sleep on the value and still wait just long enough. + """ + return max(0.0, self._expiry - asyncio.get_running_loop().time()) + + @property + def expired(self): + """ + Returns `True` if this instance of the Terms of Service has expired and should be re-fetched. + + .. code-block:: python + + if tos.expired: + tos = await client.get_tos() + """ + return asyncio.get_running_loop() >= self._expiry + + def __init__(self): + raise TypeError('You cannot create TermsOfService instances by hand!') + + @classmethod + def _new(cls, client, tos, expiry): + self = cls.__new__(cls) + self._client = client + self._tos = tos + self._expiry = expiry or asyncio.get_running_loop().time() + _DEFAULT_TIMEOUT + return self + + async def accept(self, *, age=None): + """ + Accept the Terms of Service. + + Does nothing if there is nothing to accept. + + If `minimum_age` is not `None`, the `age` parameter must be provided, + and be greater than or equal to `minimum_age`. Otherwise, the function will fail. + + .. code-example: + + if tos.minimum_age: + age = int(input('age: ')) + else: + age = None + + print(tos.html) + if input('accept (y/n)?: ') == 'y': + await tos.accept(age=age) + """ + if not self._tos: + return + + if age < (self.minimum_age or 0): + raise ValueError('User is not old enough to accept the Terms of Service') + + if age > 122: + # This easter egg may be out of date by 2025 + print('Lying is done at your own risk!', file=sys.stderr) + + await self._client(_tl.fn.help.AcceptTermsOfService(self._tos.id)) + + async def decline(self): + """ + Decline the Terms of Service. + + Does nothing if there is nothing to decline. + + .. danger:: + + Declining the Terms of Service will result in the `termination of your account`_. + **Your account will be deleted**. + + _termination of your account: https://core.telegram.org/api/config#terms-of-service + """ + if not self._tos: + return + + await self._client(_tl.fn.account.DeleteAccount('Decline ToS update')) + + def __bool__(self): + return self._tos is not None From e80371b6d00fc426850def52c0541f9b8081c1d5 Mon Sep 17 00:00:00 2001 From: Noel Tautges Date: Sun, 20 Feb 2022 02:50:18 -0600 Subject: [PATCH 228/256] Add `PathLike` support to `SQLiteSession` (#3737) --- telethon/_sessions/sqlite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/_sessions/sqlite.py b/telethon/_sessions/sqlite.py index 7b0e5849..791f4818 100644 --- a/telethon/_sessions/sqlite.py +++ b/telethon/_sessions/sqlite.py @@ -39,7 +39,7 @@ class SQLiteSession(Session): self.save_entities = True if session_id: - self.filename = session_id + self.filename = os.fspath(session_id) if not self.filename.endswith(EXTENSION): self.filename += EXTENSION From c47a9346e12546296876ba3ab3fdcde55897dac6 Mon Sep 17 00:00:00 2001 From: James R T Date: Sun, 27 Feb 2022 21:02:12 +0800 Subject: [PATCH 229/256] Fix isisintance typo (#3749) --- telethon/_misc/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/_misc/utils.py b/telethon/_misc/utils.py index 78677142..9121637a 100644 --- a/telethon/_misc/utils.py +++ b/telethon/_misc/utils.py @@ -966,7 +966,7 @@ def get_peer_id(peer): Extract the integer ID from the given :tl:`Peer`. """ pid = getattr(peer, 'user_id', None) or getattr(peer, 'channel_id', None) or getattr(peer, 'chat_id', None) - if not isisintance(pid, int): + if not isinstance(pid, int): _raise_cast_fail(peer, 'int') return pid From 26109d59167c19130c1bb840c069cc60aedf7303 Mon Sep 17 00:00:00 2001 From: Devesh Pal Date: Wed, 2 Mar 2022 11:58:17 +0000 Subject: [PATCH 230/256] Update to layer 139 (#3750) --- telethon_generator/data/api.tl | 22 +++++++++++++++++----- telethon_generator/data/errors.csv | 3 +++ telethon_generator/data/methods.csv | 4 +++- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/telethon_generator/data/api.tl b/telethon_generator/data/api.tl index 042b99d0..de179160 100644 --- a/telethon_generator/data/api.tl +++ b/telethon_generator/data/api.tl @@ -235,6 +235,8 @@ inputReportReasonOther#c1e4a2b1 = ReportReason; inputReportReasonCopyright#9b89f93a = ReportReason; inputReportReasonGeoIrrelevant#dbd4feed = ReportReason; inputReportReasonFake#f5ddd6e7 = ReportReason; +inputReportReasonIllegalDrugs#a8eb2be = ReportReason; +inputReportReasonPersonalDetails#9ec7863d = ReportReason; userFull#cf366521 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true id:long about:flags.1?string settings:PeerSettings profile_photo:flags.2?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string = UserFull; @@ -566,7 +568,7 @@ inputStickerSetAnimatedEmoji#28703c8 = InputStickerSet; inputStickerSetDice#e67f520e emoticon:string = InputStickerSet; inputStickerSetAnimatedEmojiAnimations#cde3739 = InputStickerSet; -stickerSet#d7df217a flags:# archived:flags.1?true official:flags.2?true masks:flags.3?true animated:flags.5?true gifs:flags.6?true installed_date:flags.0?int id:long access_hash:long title:string short_name:string thumbs:flags.4?Vector thumb_dc_id:flags.4?int thumb_version:flags.4?int count:int hash:int = StickerSet; +stickerSet#d7df217a flags:# archived:flags.1?true official:flags.2?true masks:flags.3?true animated:flags.5?true videos:flags.6?true installed_date:flags.0?int id:long access_hash:long title:string short_name:string thumbs:flags.4?Vector thumb_dc_id:flags.4?int thumb_version:flags.4?int count:int hash:int = StickerSet; messages.stickerSet#b60a24a6 set:StickerSet packs:Vector documents:Vector = messages.StickerSet; messages.stickerSetNotModified#d3f924eb = messages.StickerSet; @@ -1226,7 +1228,7 @@ peerBlocked#e8fd8014 peer_id:Peer date:int = PeerBlocked; stats.messageStats#8999f295 views_graph:StatsGraph = stats.MessageStats; groupCallDiscarded#7780bcb4 id:long access_hash:long duration:int = GroupCall; -groupCall#d597650c flags:# join_muted:flags.1?true can_change_join_muted:flags.2?true join_date_asc:flags.6?true schedule_start_subscribed:flags.8?true can_start_video:flags.9?true record_video_active:flags.11?true id:long access_hash:long participants_count:int title:flags.3?string stream_dc_id:flags.4?int record_start_date:flags.5?int schedule_date:flags.7?int unmuted_video_count:flags.10?int unmuted_video_limit:int version:int = GroupCall; +groupCall#d597650c flags:# join_muted:flags.1?true can_change_join_muted:flags.2?true join_date_asc:flags.6?true schedule_start_subscribed:flags.8?true can_start_video:flags.9?true record_video_active:flags.11?true rtmp_stream:flags.12?true listeners_hidden:flags.13?true id:long access_hash:long participants_count:int title:flags.3?string stream_dc_id:flags.4?int record_start_date:flags.5?int schedule_date:flags.7?int unmuted_video_count:flags.10?int unmuted_video_limit:int version:int = GroupCall; inputGroupCall#d8aa840f id:long access_hash:long = InputGroupCall; @@ -1321,6 +1323,12 @@ messages.translateResultText#a214f7d0 text:string = messages.TranslatedText; messagePeerReaction#51b67eff flags:# big:flags.0?true unread:flags.1?true peer_id:Peer reaction:string = MessagePeerReaction; +groupCallStreamChannel#80eb48af channel:int scale:int last_timestamp_ms:long = GroupCallStreamChannel; + +phone.groupCallStreamChannels#d0e482b2 channels:Vector = phone.GroupCallStreamChannels; + +phone.groupCallStreamRtmpUrl#2dbf3432 url:string key:string = phone.GroupCallStreamRtmpUrl; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1449,6 +1457,7 @@ contacts.addContact#e8f463d0 flags:# add_phone_privacy_exception:flags.0?true id contacts.acceptContact#f831a20f id:InputUser = Updates; contacts.getLocated#d348bc44 flags:# background:flags.1?true geo_point:InputGeoPoint self_expires:flags.0?int = Updates; contacts.blockFromReplies#29a8962c flags:# delete_message:flags.0?true delete_history:flags.1?true report_spam:flags.2?true msg_id:int = Updates; +contacts.resolvePhone#8af94344 phone:string = contacts.ResolvedPeer; messages.getMessages#63c66506 id:Vector = messages.Messages; messages.getDialogs#a0f4cb4f flags:# exclude_pinned:flags.0?true folder_id:flags.1?int offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:long = messages.Dialogs; @@ -1608,6 +1617,7 @@ messages.setDefaultReaction#d960c4d4 reaction:string = Bool; messages.translateText#24ce6dee flags:# peer:flags.0?InputPeer msg_id:flags.0?int text:flags.1?string from_lang:flags.2?string to_lang:string = messages.TranslatedText; messages.getUnreadReactions#e85bae1a peer:InputPeer offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; messages.readReactions#82e251d7 peer:InputPeer = messages.AffectedHistory; +messages.searchSentMedia#107e31a0 q:string filter:MessagesFilter limit:int = messages.Messages; updates.getState#edd4882a = updates.State; updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference; @@ -1704,7 +1714,7 @@ payments.getSavedInfo#227d824b = payments.SavedInfo; payments.clearSavedInfo#d83d70c1 flags:# credentials:flags.0?true info:flags.1?true = Bool; payments.getBankCardData#2e79d779 number:string = payments.BankCardData; -stickers.createStickerSet#9021ab67 flags:# masks:flags.0?true animated:flags.1?true gifs:flags.4?true user_id:InputUser title:string short_name:string thumb:flags.2?InputDocument stickers:Vector software:flags.3?string = messages.StickerSet; +stickers.createStickerSet#9021ab67 flags:# masks:flags.0?true animated:flags.1?true videos:flags.4?true user_id:InputUser title:string short_name:string thumb:flags.2?InputDocument stickers:Vector software:flags.3?string = messages.StickerSet; stickers.removeStickerFromSet#f7760f51 sticker:InputDocument = messages.StickerSet; stickers.changeStickerPosition#ffb6d4ca sticker:InputDocument position:int = messages.StickerSet; stickers.addStickerToSet#8653febe stickerset:InputStickerSet sticker:InputStickerSetItem = messages.StickerSet; @@ -1721,7 +1731,7 @@ phone.discardCall#b2cbc1c0 flags:# video:flags.0?true peer:InputPhoneCall durati phone.setCallRating#59ead627 flags:# user_initiative:flags.0?true peer:InputPhoneCall rating:int comment:string = Updates; phone.saveCallDebug#277add7e peer:InputPhoneCall debug:DataJSON = Bool; phone.sendSignalingData#ff7a9383 peer:InputPhoneCall data:bytes = Bool; -phone.createGroupCall#48cdc6d8 flags:# peer:InputPeer random_id:int title:flags.0?string schedule_date:flags.1?int = Updates; +phone.createGroupCall#48cdc6d8 flags:# rtmp_stream:flags.2?true peer:InputPeer random_id:int title:flags.0?string schedule_date:flags.1?int = Updates; phone.joinGroupCall#b132ff7b flags:# muted:flags.0?true video_stopped:flags.2?true call:InputGroupCall join_as:InputPeer invite_hash:flags.1?string params:DataJSON = Updates; phone.leaveGroupCall#500377f9 call:InputGroupCall source:int = Updates; phone.inviteToGroupCall#7b393160 call:InputGroupCall users:Vector = Updates; @@ -1740,6 +1750,8 @@ phone.startScheduledGroupCall#5680e342 call:InputGroupCall = Updates; phone.saveDefaultGroupCallJoinAs#575e1f8c peer:InputPeer join_as:InputPeer = Bool; phone.joinGroupCallPresentation#cbea6bc4 call:InputGroupCall params:DataJSON = Updates; phone.leaveGroupCallPresentation#1c50d144 call:InputGroupCall = Updates; +phone.getGroupCallStreamChannels#1ab21940 call:InputGroupCall = phone.GroupCallStreamChannels; +phone.getGroupCallStreamRtmpUrl#deb3abbf peer:InputPeer revoke:Bool = phone.GroupCallStreamRtmpUrl; langpack.getLangPack#f2f2330a lang_pack:string lang_code:string = LangPackDifference; langpack.getStrings#efea3803 lang_pack:string lang_code:string keys:Vector = Vector; @@ -1756,4 +1768,4 @@ stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel stats.getMessagePublicForwards#5630281b channel:InputChannel msg_id:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats; -// LAYER 138 +// LAYER 139 diff --git a/telethon_generator/data/errors.csv b/telethon_generator/data/errors.csv index c5d8d0ab..5584b049 100644 --- a/telethon_generator/data/errors.csv +++ b/telethon_generator/data/errors.csv @@ -160,6 +160,7 @@ GRAPH_OUTDATED_RELOAD,400,"Data can't be used for the channel statistics, graphs GROUPCALL_ADD_PARTICIPANTS_FAILED,500, GROUPCALL_ALREADY_DISCARDED,400, GROUPCALL_FORBIDDEN,403, +GROUPCALL_INVALID,400, GROUPCALL_JOIN_MISSING,400, GROUPCALL_SSRC_DUPLICATE_MUCH,400, GROUPCALL_NOT_MODIFIED,400, @@ -259,6 +260,7 @@ PHONE_CODE_EXPIRED,400,The confirmation code has expired PHONE_CODE_HASH_EMPTY,400,The phone code hash is missing PHONE_CODE_INVALID,400,The phone code entered was invalid PHONE_MIGRATE_0,303,The phone number a user is trying to use for authorization is associated with DC {new_dc} +PHONE_NOT_OCCUPIED,400, PHONE_NUMBER_APP_SIGNUP_FORBIDDEN,400,You can't sign up using this app PHONE_NUMBER_BANNED,400,The used phone number has been banned from Telegram and cannot be used anymore. Maybe check https://www.telegram.org/faq_spam PHONE_NUMBER_FLOOD,400,You asked for the code too many times. @@ -271,6 +273,7 @@ PHOTO_CONTENT_TYPE_INVALID,400, PHOTO_CONTENT_URL_EMPTY,400,The content from the URL used as a photo appears to be empty or has caused another HTTP error PHOTO_CROP_SIZE_SMALL,400,Photo is too small PHOTO_EXT_INVALID,400,The extension of the photo is invalid +PHOTO_FILE_MISSING,400, PHOTO_ID_INVALID,400,Photo id is invalid PHOTO_INVALID,400,Photo invalid PHOTO_INVALID_DIMENSIONS,400,The photo dimensions are invalid (hint: `pip install pillow` for `send_file` to resize images) diff --git a/telethon_generator/data/methods.csv b/telethon_generator/data/methods.csv index f75abb9b..0d595987 100644 --- a/telethon_generator/data/methods.csv +++ b/telethon_generator/data/methods.csv @@ -140,6 +140,7 @@ contacts.getTopPeers,user,TYPES_EMPTY contacts.importContacts,user, contacts.resetSaved,user, contacts.resetTopPeerRating,user,PEER_ID_INVALID +contacts.resolvePhone,user,PHONE_NOT_OCCUPIED contacts.resolveUsername,both,AUTH_KEY_PERM_EMPTY SESSION_PASSWORD_NEEDED USERNAME_INVALID USERNAME_NOT_OCCUPIED contacts.search,user,QUERY_TOO_SHORT SEARCH_QUERY_EMPTY TIMEOUT contacts.toggleTopPeers,user, @@ -320,6 +321,7 @@ phone.discardCall,user,CALL_ALREADY_ACCEPTED CALL_PEER_INVALID phone.discardGroupCallRequest,user,GROUPCALL_ALREADY_DISCARDED phone.editGroupCallParticipant,user,USER_VOLUME_INVALID phone.getCallConfig,user, +phone.getGroupCallStreamChannels,user,GROUPCALL_INVALID phone.inviteToGroupCall,user,GROUPCALL_FORBIDDEN USER_ALREADY_INVITED INVITE_FORBIDDEN_WITH_JOINAS phone.joinGroupCall,user,GROUPCALL_ADD_PARTICIPANTS_FAILED GROUPCALL_SSRC_DUPLICATE_MUCH phone.joinGroupCallPresentation,user, PARTICIPANT_JOIN_MISSING @@ -331,7 +333,7 @@ phone.toggleGroupCallSettings,user,GROUPCALL_NOT_MODIFIED photos.deletePhotos,user, photos.getUserPhotos,both,MAX_ID_INVALID USER_ID_INVALID photos.updateProfilePhoto,user,PHOTO_ID_INVALID -photos.uploadProfilePhoto,user,ALBUM_PHOTOS_TOO_MANY FILE_PARTS_INVALID IMAGE_PROCESS_FAILED PHOTO_CROP_SIZE_SMALL PHOTO_EXT_INVALID VIDEO_FILE_INVALID +photos.uploadProfilePhoto,user,ALBUM_PHOTOS_TOO_MANY FILE_PARTS_INVALID IMAGE_PROCESS_FAILED PHOTO_CROP_SIZE_SMALL PHOTO_EXT_INVALID PHOTO_FILE_MISSING VIDEO_FILE_INVALID ping,both, reqDHParams,both, reqPq,both, From 503d0859ecc786e830bfdd3861dfa4dca3a22580 Mon Sep 17 00:00:00 2001 From: Shrimadhav U K Date: Wed, 2 Mar 2022 17:29:40 +0530 Subject: [PATCH 231/256] Add support for both mention buttons (#3680) --- telethon/types/_custom/button.py | 52 +++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/telethon/types/_custom/button.py b/telethon/types/_custom/button.py index 0ea42d40..34897110 100644 --- a/telethon/types/_custom/button.py +++ b/telethon/types/_custom/button.py @@ -56,9 +56,9 @@ class Button: _tl.KeyboardButtonCallback, _tl.KeyboardButtonGame, _tl.KeyboardButtonSwitchInline, - _tl.KeyboardButtonUserProfile, _tl.KeyboardButtonUrl, - _tl.InputKeyboardButtonUrlAuth + _tl.InputKeyboardButtonUrlAuth, + _tl.InputKeyboardButtonUserProfile )) @staticmethod @@ -170,11 +170,18 @@ class Button: ) @staticmethod - def mention(text, input_entity=None): + def inline_mention(text, input_entity=None): """ Creates a new inline button linked to the profile of user. + This will only work in Telegram versions released after December 7, 2021. + + Older clients will display unsupported message. + Args: + text: + Label text on the button + input_entity: Input entity of :tl:User to use for profile button. By default, this is the logged in user (itself), although @@ -185,11 +192,47 @@ class Button: For now, you cannot use ID or username for this argument. If you want to use different user, you must manually use `client.get_input_entity() `. + """ return _tl.InputKeyboardButtonUserProfile( text, utils.get_input_user(input_entity or _tl.InputUserSelf()) ) + + + @staticmethod + def mention(text, input_entity=None): + """ + Creates a text mentioning the user. + + This will only work in Telegram versions (only Telegram Desktop and Telegram X) released after December 7, 2021. + + Older clients will display unsupported message. + + Args: + text: + Label text on the button + + input_entity: + Input entity of :tl:User to use for profile button. + By default, this is the logged in user (itself), although + you may pass a different input peer. + + .. note:: + + For now, you cannot use ID or username for this argument. + If you want to use different user, you must manually use + `client.get_input_entity() `. + + """ + return types.KeyboardButtonUserProfile( + text, + ( + utils.get_input_user( + input_entity or _tl.InputUserSelf() + ).id + ) + ) @classmethod @@ -413,4 +456,5 @@ def build_reply_markup( return _tl.ReplyInlineMarkup(rows) # elif is_normal: return _tl.ReplyKeyboardMarkup( - rows, resize=resize, single_use=single_use, selective=selective) \ No newline at end of file + rows, resize=resize, single_use=single_use, selective=selective) + From ab150bf457612ad97276026cfd1f3636d9de2986 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 18 Feb 2022 19:09:14 +0100 Subject: [PATCH 232/256] Fix get_tos usage of expiry expires is actually a timestamp, not seconds --- telethon/_client/auth.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telethon/_client/auth.py b/telethon/_client/auth.py index 518abc8d..4a4ec1d4 100644 --- a/telethon/_client/auth.py +++ b/telethon/_client/auth.py @@ -6,6 +6,7 @@ import sys import typing import warnings import functools +import time import dataclasses from .._misc import utils, helpers, password as pwd_mod @@ -282,7 +283,7 @@ async def get_tos(self): if first_time or no_tos or tos_expired: result = await self(_tl.fn.help.GetTermsOfServiceUpdate()) tos = getattr(result, 'terms_of_service', None) - self._tos = (tos, asyncio.get_running_loop().time() + result.expires) + self._tos = (tos, asyncio.get_running_loop().time() + result.expires.timestamp() - time.time()) # not stored in the client to prevent a cycle return _custom.TermsOfService._new(self, *self._tos) From dd2f83b7acc35dcc8beeffb9d66b15398dbf2044 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 18 Feb 2022 19:09:19 +0100 Subject: [PATCH 233/256] Add str and repr to ToS --- telethon/types/_custom/tos.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/telethon/types/_custom/tos.py b/telethon/types/_custom/tos.py index 7b4f4c34..43ece84c 100644 --- a/telethon/types/_custom/tos.py +++ b/telethon/types/_custom/tos.py @@ -156,5 +156,11 @@ class TermsOfService: await self._client(_tl.fn.account.DeleteAccount('Decline ToS update')) + def __str__(self): + return self.markdown or '(empty ToS)' + + def __repr__(self): + return f'TermsOfService({self.markdown!r})' + def __bool__(self): return self._tos is not None From d6669a11726e7ab053c50ec5ba411ba20666d96d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 18 Feb 2022 19:21:45 +0100 Subject: [PATCH 234/256] Fix ToS imports --- telethon/types/_custom/tos.py | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/telethon/types/_custom/tos.py b/telethon/types/_custom/tos.py index 43ece84c..390d8aa0 100644 --- a/telethon/types/_custom/tos.py +++ b/telethon/types/_custom/tos.py @@ -1,19 +1,8 @@ import sys +import asyncio -from typing import Optional, List, TYPE_CHECKING -from datetime import datetime -from dataclasses import dataclass -import mimetypes -from .chatgetter import ChatGetter -from .sendergetter import SenderGetter -from .messagebutton import MessageButton -from .forward import Forward -from .file import File -from .inputfile import InputFile -from .inputmessage import InputMessage -from .button import build_reply_markup -from ..._misc import utils, helpers, tlobject, markdown, html -from ... import _tl, _misc +from ..._misc import markdown, html +from ... import _tl _DEFAULT_TIMEOUT = 24 * 60 * 60 @@ -93,7 +82,7 @@ class TermsOfService: if tos.expired: tos = await client.get_tos() """ - return asyncio.get_running_loop() >= self._expiry + return asyncio.get_running_loop().time() >= self._expiry def __init__(self): raise TypeError('You cannot create TermsOfService instances by hand!') From d1dba60278c8a34af980e9f6fdce09f741db68c8 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 18 Feb 2022 19:21:52 +0100 Subject: [PATCH 235/256] Fix begin/end_takeout --- telethon/_client/account.py | 14 ++++++++------ telethon/_client/telegramclient.py | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/telethon/_client/account.py b/telethon/_client/account.py index cdf79850..4451a0a3 100644 --- a/telethon/_client/account.py +++ b/telethon/_client/account.py @@ -2,6 +2,7 @@ import functools import inspect import typing import dataclasses +import asyncio from contextvars import ContextVar from .._misc import helpers, utils @@ -22,7 +23,7 @@ class _Takeout: self._kwargs = kwargs async def __aenter__(self): - await self._client.begin_takeout(**kwargs) + await self._client.begin_takeout(**self._kwargs) return self._client async def __aexit__(self, exc_type, exc_value, traceback): @@ -44,10 +45,10 @@ async def begin_takeout( files: bool = None, max_file_size: bool = None, ) -> 'TelegramClient': - if takeout_active(): + if self.takeout_active: raise ValueError('a previous takeout session was already active') - await self._replace_session_state(takeout_id=(await client( + takeout = await self(_tl.fn.account.InitTakeoutSession( contacts=contacts, message_users=users, message_chats=chats, @@ -55,15 +56,16 @@ async def begin_takeout( message_channels=channels, files=files, file_max_size=max_file_size - )).id) + )) + await self._replace_session_state(takeout_id=takeout.id) def takeout_active(self: 'TelegramClient') -> bool: return self._session_state.takeout_id is not None -async def end_takeout(self: 'TelegramClient', success: bool) -> bool: - if not takeout_active(): +async def end_takeout(self: 'TelegramClient', *, success: bool) -> bool: + if not self.takeout_active: raise ValueError('no previous takeout session was active') result = await self(_tl.fn.account.FinishTakeoutSession(success)) diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index c8a41c92..e38f237e 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -289,7 +289,7 @@ class TelegramClient: return account.takeout_active(self) @forward_call(account.end_takeout) - async def end_takeout(self: 'TelegramClient', success: bool) -> bool: + async def end_takeout(self: 'TelegramClient', *, success: bool) -> bool: """ Finishes the current takeout session. From 48bd0615623edf8fca5c35d6f79801fd766e7045 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 24 Feb 2022 10:59:52 +0100 Subject: [PATCH 236/256] Fix calls to remove_event_handler --- telethon/_client/telegramclient.py | 1 + telethon/_client/updates.py | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index e38f237e..b4efb9c7 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -2877,6 +2877,7 @@ class TelegramClient: self: 'TelegramClient', callback: updates.Callback = None, event: EventBuilder = None, + *, priority=None, ) -> int: """ diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index 91e0b05e..23d9ee27 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -97,9 +97,10 @@ def add_event_handler( def remove_event_handler( self: 'TelegramClient', - callback, - event, - priority, + callback=None, + event=None, + *, + priority=None, ): if callback is None and event is None and priority is None: raise ValueError('must specify at least one of callback, event or priority') From d5cdda28c527f1e1b725f9ba15682a6cc630aa91 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 24 Feb 2022 11:07:13 +0100 Subject: [PATCH 237/256] Change QrLogin to reduce room for error --- readthedocs/misc/v2-migration-guide.rst | 1 + readthedocs/modules/custom.rst | 2 +- telethon/_client/auth.py | 7 +- telethon/_client/telegramclient.py | 24 +++-- telethon/types/__init__.py | 2 +- telethon/types/_custom/__init__.py | 2 +- telethon/types/_custom/qrlogin.py | 134 ++++++++++++++++-------- 7 files changed, 115 insertions(+), 57 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index a43f3ffa..20a3cfdd 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -984,3 +984,4 @@ sign_in no longer has phone or phone_hash (these are impl details, and now it's send code / sign in now only expect a single phone. resend code with new phone is send code, not resend. sign_up code is also now a kwarg. and no longer noop if already loggedin. start also mandates phone= or password= as kwarg. +qrlogin expires has been replaced with timeout and expired for parity with tos and auth. the goal is to hide the error-prone system clock and instead use asyncio's clock. recreate was removed (just call qr_login again; parity with get_tos). class renamed to QrLogin. now must be used in a contextmgr to prevent misuse. diff --git a/readthedocs/modules/custom.rst b/readthedocs/modules/custom.rst index 01284fbb..22409f05 100644 --- a/readthedocs/modules/custom.rst +++ b/readthedocs/modules/custom.rst @@ -136,7 +136,7 @@ ParticipantPermissions :show-inheritance: -QRLogin +QrLogin ======= .. automodule:: telethon.tl.custom.qrlogin diff --git a/telethon/_client/auth.py b/telethon/_client/auth.py index 4a4ec1d4..2a3e3af3 100644 --- a/telethon/_client/auth.py +++ b/telethon/_client/auth.py @@ -342,10 +342,9 @@ async def send_code_request( return _custom.SentCode._new(result) -async def qr_login(self: 'TelegramClient', ignored_ids: typing.List[int] = None) -> _custom.QRLogin: - qr_login = _custom.QRLogin(self, ignored_ids or []) - await qr_login.recreate() - return qr_login +def qr_login(self: 'TelegramClient', ignored_ids: typing.List[int] = None) -> _custom.QrLogin: + return _custom.QrLoginManager(self, ignored_ids) + async def log_out(self: 'TelegramClient') -> bool: try: diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index b4efb9c7..8c3cf268 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -531,7 +531,7 @@ class TelegramClient: """ @forward_call(auth.qr_login) - async def qr_login(self: 'TelegramClient', ignored_ids: typing.List[int] = None) -> _custom.QRLogin: + def qr_login(self: 'TelegramClient', ignored_ids: typing.List[int] = None) -> _custom.QrLogin: """ Initiates the QR login procedure. @@ -542,15 +542,18 @@ class TelegramClient: whether it's the URL, using the token bytes directly, or generating a QR code and displaying it by other means. - See the documentation for `QRLogin` to see how to proceed after this. + See the documentation for `QrLogin` to see how to proceed after this. + + Note that the login completes once the context manager exits, + not after the ``wait`` method returns. Arguments ignored_ids (List[`int`]): - List of already logged-in user IDs, to prevent logging in + List of already logged-in session IDs, to prevent logging in twice with the same user. Returns - An instance of `QRLogin`. + An instance of `QrLogin`. Example .. code-block:: python @@ -558,11 +561,16 @@ class TelegramClient: def display_url_as_qr(url): pass # do whatever to show url as a qr to the user - qr_login = await client.qr_login() - display_url_as_qr(qr_login.url) + async with client.qr_login() as qr_login: + display_url_as_qr(qr_login.url) - # Important! You need to wait for the login to complete! - await qr_login.wait() + # Important! You need to wait for the login to complete! + # If the context manager exits before the user logs in, the client won't be logged in. + try: + user = await qr_login.wait() + print('Welcome,', user.first_name) + except asyncio.TimeoutError: + print('User did not login in time') """ @forward_call(auth.log_out) diff --git a/telethon/types/__init__.py b/telethon/types/__init__.py index 39f8520e..334e4161 100644 --- a/telethon/types/__init__.py +++ b/telethon/types/__init__.py @@ -13,7 +13,7 @@ from ._custom import ( InlineBuilder, InlineResult, InlineResults, - QRLogin, + QrLogin, ParticipantPermissions, Chat, User, diff --git a/telethon/types/_custom/__init__.py b/telethon/types/_custom/__init__.py index d8945804..76f372b1 100644 --- a/telethon/types/_custom/__init__.py +++ b/telethon/types/_custom/__init__.py @@ -10,7 +10,7 @@ from .button import Button from .inlinebuilder import InlineBuilder from .inlineresult import InlineResult from .inlineresults import InlineResults -from .qrlogin import QRLogin +from .qrlogin import QrLoginManager, QrLogin from .participantpermissions import ParticipantPermissions from .chat import Chat from .user import User diff --git a/telethon/types/_custom/qrlogin.py b/telethon/types/_custom/qrlogin.py index 473e4bd0..be1fec55 100644 --- a/telethon/types/_custom/qrlogin.py +++ b/telethon/types/_custom/qrlogin.py @@ -1,30 +1,75 @@ import asyncio import base64 -import datetime +import time +import functools from ... import _tl from ..._events.raw import Raw -class QRLogin: +class QrLoginManager: + def __init__(self, client, ignored_ids): + self._client = client + self._request = _tl.fn.auth.ExportLoginToken(client._api_id, client._api_hash, ignored_ids or []) + self._event = None + self._handler = None + self._login = None + + async def __aenter__(self): + self._event = asyncio.Event() + self._handler = self._client.add_event_handler(self._callback, Raw) + + try: + qr = await self._client(self._request) + except: + self._cleanup() + raise + + self._login = QrLogin._new(self._client, self._request, qr, self._event) + return self._login + + async def __aexit__(self, *args): + try: + # The logic to complete the login is in wait so the user can retrieve the logged-in user + await self._login.wait(timeout=0) + # User logged-in in time + except asyncio.TimeoutError: + pass # User did not login in time + finally: + self._cleanup() + + async def _callback(self, update): + if isinstance(update, _tl.UpdateLoginToken): + self._event.set() + + def _cleanup(self): + # Users technically could remove all raw handlers during the procedure but it's unlikely to happen + self._client.remove_event_handler(self._handler) + self._event = None + self._handler = None + self._login = None + + +class QrLogin: """ QR login information. Most of the time, you will present the `url` as a QR code to the user, and while it's being shown, call `wait`. """ - def __init__(self, client, ignored_ids): - self._client = client - self._request = _tl.fn.auth.ExportLoginToken( - self._client.api_id, self._client.api_hash, ignored_ids) - self._resp = None + def __init__(self): + raise TypeError('You cannot create QrLogin instances by hand!') - async def recreate(self): - """ - Generates a new token and URL for a new QR code, useful if the code - has expired before it was imported. - """ - self._resp = await self._client(self._request) + @classmethod + def _new(cls, client, request, qr, event): + self = cls.__new__(cls) + self._client = client + self._request = request + self._qr = qr + self._expiry = asyncio.get_running_loop().time() + qr.expires.timestamp() - time.time() + self._event = event + self._user = None + return self @property def token(self) -> bytes: @@ -35,7 +80,7 @@ class QRLogin: :tl:`auth.importLoginToken` to log the client that originally requested the QR login. """ - return self._resp.token + return self._qr.token @property def url(self) -> str: @@ -54,16 +99,30 @@ class QRLogin: The URL simply consists of `token` base64-encoded. """ - return 'tg://login?token={}'.format(base64.urlsafe_b64encode(self._resp.token).decode('utf-8').rstrip('=')) + return 'tg://login?token={}'.format(base64.urlsafe_b64encode(self._qr.token).decode('utf-8').rstrip('=')) @property - def expires(self) -> datetime.datetime: + def timeout(self): """ - The `datetime` at which the QR code will expire. + How many seconds are left before `client.qr_login` should be used again. - If you want to try again, you will need to call `recreate`. + This value is a positive floating point number, and is monotically decreasing. + The value will reach zero after enough seconds have elapsed. This lets you do some work + and call sleep on the value and still wait just long enough. """ - return self._resp.expires + return max(0.0, self._expiry - asyncio.get_running_loop().time()) + + @property + def expired(self): + """ + Returns `True` if this instance of the QR login has expired and should be re-created. + + .. code-block:: python + + if qr.expired: + qr = await client.qr_login() + """ + return asyncio.get_running_loop().time() >= self._expiry async def wait(self, timeout: float = None): """ @@ -71,13 +130,12 @@ class QRLogin: either by scanning the QR, launching the URL directly, or calling the import method. - This method **must** be called before the QR code is scanned, and - must be executing while the QR code is being scanned. Otherwise, the - login will not complete. - Will raise `asyncio.TimeoutError` if the login doesn't complete on time. + Note that the login can complete even if `wait` isn't used (if the + context-manager is kept alive for long enough and the users logs in). + Arguments timeout (float): The timeout, in seconds, to wait before giving up. By default @@ -85,26 +143,18 @@ class QRLogin: what you want. Returns - On success, an instance of :tl:`User`. On failure it will raise. + On success, an instance of `User`. On failure it will raise. """ + if self._user: + return self._user + if timeout is None: - timeout = (self._resp.expires - datetime.datetime.now(tz=datetime.timezone.utc)).total_seconds() + timeout = self.timeout - event = asyncio.Event() + # Will raise timeout error if it doesn't complete quick enough, + # which we want to let propagate + await asyncio.wait_for(self._event.wait(), timeout=timeout) - async def handler(_update): - event.set() - - self._client.add_event_handler(handler, Raw(_tl.UpdateLoginToken)) - - try: - # Will raise timeout error if it doesn't complete quick enough, - # which we want to let propagate - await asyncio.wait_for(event.wait(), timeout=timeout) - finally: - self._client.remove_event_handler(handler) - - # We got here without it raising timeout error, so we can proceed resp = await self._client(self._request) if isinstance(resp, _tl.auth.LoginTokenMigrateTo): await self._client._switch_dc(resp.dc_id) @@ -113,7 +163,7 @@ class QRLogin: if isinstance(resp, _tl.auth.LoginTokenSuccess): user = resp.authorization.user - self._client._on_login(user) - return user + self._user = self._client._update_session_state(user) + return self._user - raise TypeError('Login token response was unexpected: {}'.format(resp)) + raise RuntimeError(f'Unexpected login token response: {resp}') From a76c9e9f4ced3c62936ee9b8c638774295b2f653 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 24 Feb 2022 11:10:35 +0100 Subject: [PATCH 238/256] Fix non-obvious error when not connected --- telethon/_client/telegrambaseclient.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/telethon/_client/telegrambaseclient.py b/telethon/_client/telegrambaseclient.py index ec2a0ee5..52e5c392 100644 --- a/telethon/_client/telegrambaseclient.py +++ b/telethon/_client/telegrambaseclient.py @@ -132,7 +132,7 @@ def init( self._session = session # In-memory copy of the session's state to avoid a roundtrip as it contains commonly-accessed values. - self._session_state = None + self._session_state = _default_session_state() # Nice-to-have. self._request_retries = request_retries @@ -219,22 +219,26 @@ def set_flood_sleep_threshold(self, value): self._flood_sleep_threshold = min(value or 0, 24 * 60 * 60) +def _default_session_state(): + return SessionState( + user_id=0, + dc_id=DEFAULT_DC_ID, + bot=False, + pts=0, + qts=0, + date=0, + seq=0, + takeout_id=None, + ) + + async def connect(self: 'TelegramClient') -> None: all_dcs = {dc.id: dc for dc in await self._session.get_all_dc()} self._session_state = await self._session.get_state() if self._session_state is None: try_fetch_user = False - self._session_state = SessionState( - user_id=0, - dc_id=DEFAULT_DC_ID, - bot=False, - pts=0, - qts=0, - date=0, - seq=0, - takeout_id=None, - ) + self._session_state = _default_session_state() else: try_fetch_user = self._session_state.user_id == 0 if self._catch_up: From 4c94407b79244226c717fa92be943d98a8c58676 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 24 Feb 2022 11:46:32 +0100 Subject: [PATCH 239/256] Fix access to properties in User and Chat --- telethon/types/_custom/chat.py | 4 ++-- telethon/types/_custom/user.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/telethon/types/_custom/chat.py b/telethon/types/_custom/chat.py index a0031bc1..f5056eff 100644 --- a/telethon/types/_custom/chat.py +++ b/telethon/types/_custom/chat.py @@ -20,10 +20,10 @@ if TYPE_CHECKING: def _fwd(field, doc): def fget(self): - return getattr(self._message, field, None) + return getattr(self._chat, field, None) def fset(self, value): - object.__setattr__(self._message, field, value) + object.__setattr__(self._chat, field, value) return property(fget, fset, None, doc) diff --git a/telethon/types/_custom/user.py b/telethon/types/_custom/user.py index 12f2e765..7a636dfd 100644 --- a/telethon/types/_custom/user.py +++ b/telethon/types/_custom/user.py @@ -20,10 +20,10 @@ if TYPE_CHECKING: def _fwd(field, doc): def fget(self): - return getattr(self._message, field, None) + return getattr(self._user, field, None) def fset(self, value): - object.__setattr__(self._message, field, value) + object.__setattr__(self._user, field, value) return property(fget, fset, None, doc) From 392808b950f68d8cfe0d577a68ae4acb048cdc4d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 24 Feb 2022 11:52:39 +0100 Subject: [PATCH 240/256] Handle GapError in updates loop --- telethon/_client/updates.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index 23d9ee27..5240e2f6 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -201,7 +201,11 @@ async def _update_loop(self: 'TelegramClient'): continue processed = [] - users, chats = self._message_box.process_updates(updates, self._entity_cache, processed) + try: + users, chats = self._message_box.process_updates(updates, self._entity_cache, processed) + except GapError: + continue # get(_channel)_difference will start returning requests + updates_to_dispatch.extend(_preprocess_updates(self, processed, users, chats)) except Exception: self._log[__name__].exception('Fatal error handling updates (this is a bug in Telethon, please report it)') From c3cefef37c6c6fcf048dec637343ce501af13b09 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 2 Mar 2022 09:00:39 +0100 Subject: [PATCH 241/256] Rename entity parameter with something clearer --- readthedocs/misc/v2-migration-guide.rst | 2 + telethon/_client/bots.py | 8 +- telethon/_client/chats.py | 36 +++--- telethon/_client/dialogs.py | 16 +-- telethon/_client/downloads.py | 10 +- telethon/_client/messages.py | 44 +++---- telethon/_client/telegramclient.py | 158 ++++++++++++------------ telethon/_client/uploads.py | 4 +- 8 files changed, 142 insertions(+), 136 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 20a3cfdd..8bad852b 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -985,3 +985,5 @@ send code / sign in now only expect a single phone. resend code with new phone i sign_up code is also now a kwarg. and no longer noop if already loggedin. start also mandates phone= or password= as kwarg. qrlogin expires has been replaced with timeout and expired for parity with tos and auth. the goal is to hide the error-prone system clock and instead use asyncio's clock. recreate was removed (just call qr_login again; parity with get_tos). class renamed to QrLogin. now must be used in a contextmgr to prevent misuse. +"entity" parameters have been renamed to "dialog" (user or chat expected) or "chat" (only chats expected), "profile" (if that makes sense). the goal is to move away from the entity terminology. this is intended to be a documentation change, but because the parameters were renamed, it's breaking. the expected usage of positional arguments is mostly unaffected. this includes the EntityLike hint. +download_media param renamed message to media. iter_download file to media too diff --git a/telethon/_client/bots.py b/telethon/_client/bots.py index 2e3ef1b6..a64fe810 100644 --- a/telethon/_client/bots.py +++ b/telethon/_client/bots.py @@ -13,12 +13,12 @@ async def inline_query( bot: 'hints.EntityLike', query: str, *, - entity: 'hints.EntityLike' = None, + dialog: 'hints.EntityLike' = None, offset: str = None, geo_point: '_tl.GeoPoint' = None) -> _custom.InlineResults: bot = await self.get_input_entity(bot) - if entity: - peer = await self.get_input_entity(entity) + if dialog: + peer = await self.get_input_entity(dialog) else: peer = _tl.InputPeerEmpty() @@ -30,4 +30,4 @@ async def inline_query( geo_point=geo_point )) - return _custom.InlineResults(self, result, entity=peer if entity else None) + return _custom.InlineResults(self, result, entity=peer if dialog else None) diff --git a/telethon/_client/chats.py b/telethon/_client/chats.py index f17d72db..f45e9bf5 100644 --- a/telethon/_client/chats.py +++ b/telethon/_client/chats.py @@ -372,7 +372,7 @@ class _ProfilePhotoIter(requestiter.RequestIter): def get_participants( self: 'TelegramClient', - entity: 'hints.EntityLike', + chat: 'hints.EntityLike', limit: float = (), *, search: str = '', @@ -380,7 +380,7 @@ def get_participants( return _ParticipantsIter( self, limit, - entity=entity, + entity=chat, filter=filter, search=search ) @@ -388,7 +388,7 @@ def get_participants( def get_admin_log( self: 'TelegramClient', - entity: 'hints.EntityLike', + chat: 'hints.EntityLike', limit: float = (), *, max_id: int = 0, @@ -413,7 +413,7 @@ def get_admin_log( return _AdminLogIter( self, limit, - entity=entity, + entity=chat, admins=admins, search=search, min_id=min_id, @@ -438,7 +438,7 @@ def get_admin_log( def get_profile_photos( self: 'TelegramClient', - entity: 'hints.EntityLike', + profile: 'hints.EntityLike', limit: int = (), *, offset: int = 0, @@ -446,7 +446,7 @@ def get_profile_photos( return _ProfilePhotoIter( self, limit, - entity=entity, + entity=profile, offset=offset, max_id=max_id ) @@ -454,7 +454,7 @@ def get_profile_photos( def action( self: 'TelegramClient', - entity: 'hints.EntityLike', + dialog: 'hints.EntityLike', action: 'typing.Union[str, _tl.TypeSendMessageAction]', *, delay: float = 4, @@ -462,11 +462,11 @@ def action( action = _ChatAction._parse(action) return _ChatAction( - self, entity, action, delay=delay, auto_cancel=auto_cancel) + self, dialog, action, delay=delay, auto_cancel=auto_cancel) async def edit_admin( self: 'TelegramClient', - entity: 'hints.EntityLike', + chat: 'hints.EntityLike', user: 'hints.EntityLike', *, change_info: bool = None, @@ -481,7 +481,7 @@ async def edit_admin( anonymous: bool = None, is_admin: bool = None, title: str = None) -> _tl.Updates: - entity = await self.get_input_entity(entity) + entity = await self.get_input_entity(chat) user = await self.get_input_entity(user) ty = helpers._entity_type(user) @@ -529,7 +529,7 @@ async def edit_admin( async def edit_permissions( self: 'TelegramClient', - entity: 'hints.EntityLike', + chat: 'hints.EntityLike', user: 'typing.Optional[hints.EntityLike]' = None, until_date: 'hints.DateLike' = None, *, @@ -545,7 +545,7 @@ async def edit_permissions( change_info: bool = True, invite_users: bool = True, pin_messages: bool = True) -> _tl.Updates: - entity = await self.get_input_entity(entity) + entity = await self.get_input_entity(chat) ty = helpers._entity_type(entity) rights = _tl.ChatBannedRights( @@ -583,10 +583,10 @@ async def edit_permissions( async def kick_participant( self: 'TelegramClient', - entity: 'hints.EntityLike', + chat: 'hints.EntityLike', user: 'typing.Optional[hints.EntityLike]' ): - entity = await self.get_input_entity(entity) + entity = await self.get_input_entity(chat) user = await self.get_input_entity(user) ty = helpers._entity_type(entity) @@ -617,10 +617,10 @@ async def kick_participant( async def get_permissions( self: 'TelegramClient', - entity: 'hints.EntityLike', + chat: 'hints.EntityLike', user: 'hints.EntityLike' = None ) -> 'typing.Optional[_custom.ParticipantPermissions]': - entity = await self.get_entity(entity) + entity = await self.get_entity(chat) if not user: if helpers._entity_type(entity) != helpers._EntityType.USER: @@ -650,10 +650,10 @@ async def get_permissions( async def get_stats( self: 'TelegramClient', - entity: 'hints.EntityLike', + chat: 'hints.EntityLike', message: 'typing.Union[int, _tl.Message]' = None, ): - entity = await self.get_input_entity(entity) + entity = await self.get_input_entity(chat) message = utils.get_message_id(message) if message is not None: diff --git a/telethon/_client/dialogs.py b/telethon/_client/dialogs.py index fc5f2447..8151f5dc 100644 --- a/telethon/_client/dialogs.py +++ b/telethon/_client/dialogs.py @@ -165,20 +165,20 @@ def get_dialogs( def get_drafts( self: 'TelegramClient', - entity: 'hints.EntitiesLike' = None + dialog: 'hints.EntitiesLike' = None ) -> _DraftsIter: limit = None - if entity: - if not utils.is_list_like(entity): - entity = (entity,) - limit = len(entity) + if dialog: + if not utils.is_list_like(dialog): + dialog = (dialog,) + limit = len(dialog) - return _DraftsIter(self, limit, entities=entity) + return _DraftsIter(self, limit, entities=dialog) async def delete_dialog( self: 'TelegramClient', - entity: 'hints.EntityLike', + dialog: 'hints.EntityLike', *, revoke: bool = False ): @@ -189,7 +189,7 @@ async def delete_dialog( else: deactivated = False - entity = await self.get_input_entity(entity) + entity = await self.get_input_entity(dialog) ty = helpers._entity_type(entity) if ty == helpers._EntityType.CHANNEL: return await self(_tl.fn.channels.LeaveChannel(entity)) diff --git a/telethon/_client/downloads.py b/telethon/_client/downloads.py index 67c4622d..35f1608e 100644 --- a/telethon/_client/downloads.py +++ b/telethon/_client/downloads.py @@ -178,7 +178,7 @@ class _GenericDownloadIter(_DirectDownloadIter): async def download_profile_photo( self: 'TelegramClient', - entity: 'hints.EntityLike', + profile: 'hints.EntityLike', file: 'hints.FileLike' = None, *, thumb, @@ -188,6 +188,7 @@ async def download_profile_photo( ENTITIES = (0x2da17977, 0xc5af5d94, 0x1f4661b9, 0xd49a2697) # ('InputPeer', 'InputUser', 'InputChannel') INPUTS = (0xc91c90b6, 0xe669bf46, 0x40f202fd) + entity = profile if not isinstance(entity, tlobject.TLObject) or entity.SUBCLASS_OF_ID in INPUTS: entity = await self.get_entity(entity) @@ -258,7 +259,7 @@ async def download_profile_photo( async def download_media( self: 'TelegramClient', - message: 'hints.MessageLike', + media: 'hints.MessageLike', file: 'hints.FileLike' = None, *, size = (), @@ -269,6 +270,7 @@ async def download_media( msg_data = None # TODO This won't work for messageService + message = media if isinstance(message, _tl.Message): date = message.date media = message.media @@ -405,7 +407,7 @@ async def _download_file( def iter_download( self: 'TelegramClient', - file: 'hints.FileLike', + media: 'hints.FileLike', *, offset: int = 0, stride: int = None, @@ -417,7 +419,7 @@ def iter_download( ): return _iter_download( self, - file, + media, offset=offset, stride=stride, limit=limit, diff --git a/telethon/_client/messages.py b/telethon/_client/messages.py index 2e89ffd8..da8fbf5c 100644 --- a/telethon/_client/messages.py +++ b/telethon/_client/messages.py @@ -332,7 +332,7 @@ async def _get_peer(self: 'TelegramClient', input_peer: 'hints.EntityLike'): def get_messages( self: 'TelegramClient', - entity: 'hints.EntityLike', + dialog: 'hints.EntityLike', limit: float = (), *, offset_date: 'hints.DateLike' = None, @@ -358,7 +358,7 @@ def get_messages( reverse=reverse, wait_time=wait_time, limit=len(ids), - entity=entity, + entity=dialog, ids=ids ) @@ -367,7 +367,7 @@ def get_messages( reverse=reverse, wait_time=wait_time, limit=limit, - entity=entity, + entity=dialog, offset_id=offset_id, min_id=min_id, max_id=max_id, @@ -396,7 +396,7 @@ async def _get_comment_data( async def send_message( self: 'TelegramClient', - entity: 'hints.EntityLike', + dialog: 'hints.EntityLike', message: 'hints.MessageLike' = '', *, # - Message contents @@ -466,7 +466,7 @@ async def send_message( elif not isinstance(message, InputMessage): raise TypeError(f'message must be either str, Message or InputMessage, but got: {message!r}') - entity = await self.get_input_entity(entity) + entity = await self.get_input_entity(dialog) if comment_to is not None: entity, reply_to = await _get_comment_data(self, entity, comment_to) elif reply_to: @@ -525,9 +525,9 @@ async def send_message( async def forward_messages( self: 'TelegramClient', - entity: 'hints.EntityLike', + dialog: 'hints.EntityLike', messages: 'typing.Union[typing.Sequence[hints.MessageIDLike]]', - from_peer: 'hints.EntityLike' = None, + from_dialog: 'hints.EntityLike' = None, *, background: bool = None, with_my_score: bool = None, @@ -540,12 +540,13 @@ async def forward_messages( if as_album is not None: warnings.warn('the as_album argument is deprecated and no longer has any effect') - entity = await self.get_input_entity(entity) + entity = await self.get_input_entity(dialog) - if from_peer: - from_peer = await self.get_input_entity(from_peer) + if from_dialog: + from_peer = await self.get_input_entity(from_dialog) from_peer_id = await self.get_peer_id(from_peer) else: + from_peer = None from_peer_id = None def get_key(m): @@ -587,7 +588,7 @@ async def forward_messages( async def edit_message( self: 'TelegramClient', - entity: 'typing.Union[hints.EntityLike, _tl.Message]', + dialog: 'typing.Union[hints.EntityLike, _tl.Message]', message: 'hints.MessageLike' = None, text: str = None, *, @@ -631,7 +632,7 @@ async def edit_message( else: return await self(request) - entity = await self.get_input_entity(entity) + entity = await self.get_input_entity(dialog) request = _tl.fn.messages.EditMessage( peer=entity, id=utils.get_message_id(message), @@ -647,7 +648,7 @@ async def edit_message( async def delete_messages( self: 'TelegramClient', - entity: 'hints.EntityLike', + dialog: 'hints.EntityLike', messages: 'typing.Union[typing.Sequence[hints.MessageIDLike]]', *, revoke: bool = True) -> 'typing.Sequence[_tl.messages.AffectedMessages]': @@ -657,11 +658,12 @@ async def delete_messages( else int(m) for m in messages ) - if entity: - entity = await self.get_input_entity(entity) + if dialog: + entity = await self.get_input_entity(dialog) ty = helpers._entity_type(entity) else: # no entity (None), set a value that's not a channel for private delete + entity = None ty = helpers._EntityType.USER if ty == helpers._EntityType.CHANNEL: @@ -675,7 +677,7 @@ async def delete_messages( async def mark_read( self: 'TelegramClient', - entity: 'hints.EntityLike', + dialog: 'hints.EntityLike', message: 'hints.MessageIDLike' = None, *, clear_mentions: bool = False, @@ -687,7 +689,7 @@ async def mark_read( else: max_id = message.id - entity = await self.get_input_entity(entity) + entity = await self.get_input_entity(dialog) if clear_mentions: await self(_tl.fn.messages.ReadMentions(entity)) @@ -705,22 +707,22 @@ async def mark_read( async def pin_message( self: 'TelegramClient', - entity: 'hints.EntityLike', + dialog: 'hints.EntityLike', message: 'typing.Optional[hints.MessageIDLike]', *, notify: bool = False, pm_oneside: bool = False ): - return await _pin(self, entity, message, unpin=False, notify=notify, pm_oneside=pm_oneside) + return await _pin(self, dialog, message, unpin=False, notify=notify, pm_oneside=pm_oneside) async def unpin_message( self: 'TelegramClient', - entity: 'hints.EntityLike', + dialog: 'hints.EntityLike', message: 'typing.Optional[hints.MessageIDLike]' = None, *, notify: bool = False ): - return await _pin(self, entity, message, unpin=True, notify=notify) + return await _pin(self, dialog, message, unpin=True, notify=notify) async def _pin(self, entity, message, *, unpin, notify=False, pm_oneside=False): message = utils.get_message_id(message) or 0 diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 8c3cf268..7a9f767f 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -704,7 +704,7 @@ class TelegramClient: bot: 'hints.EntityLike', query: str, *, - entity: 'hints.EntityLike' = None, + dialog: 'hints.EntityLike' = None, offset: str = None, geo_point: '_tl.GeoPoint' = None) -> _custom.InlineResults: """ @@ -712,17 +712,17 @@ class TelegramClient: Arguments bot (`entity`): - The bot entity to which the inline query should be made. + The bot user to which the inline query should be made. 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 + dialog (`entity`, optional): + The dialog 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 + If specified, it will also be the default dialog where the message will be sent after clicked. Otherwise, the "empty peer" will be used, which some bots may not handle correctly. @@ -754,7 +754,7 @@ class TelegramClient: @forward_call(chats.get_participants) def get_participants( self: 'TelegramClient', - entity: 'hints.EntityLike', + chat: 'hints.EntityLike', limit: float = (), *, search: str = '', @@ -765,8 +765,8 @@ class TelegramClient: The order is unspecified. Arguments - entity (`entity`): - The entity from which to retrieve the participants list. + chat (`entity`): + The chat from which to retrieve the participants list. limit (`int`): Limits amount of participants fetched. @@ -822,7 +822,7 @@ class TelegramClient: @forward_call(chats.get_admin_log) def get_admin_log( self: 'TelegramClient', - entity: 'hints.EntityLike', + chat: 'hints.EntityLike', limit: float = (), *, max_id: int = 0, @@ -856,8 +856,8 @@ class TelegramClient: `True`, only those that are true will be returned. Arguments - entity (`entity`): - The channel entity from which to get its admin log. + chat (`entity`): + The chat from which to get its admin log. limit (`int` | `None`, optional): Number of events to be retrieved. @@ -957,7 +957,7 @@ class TelegramClient: @forward_call(chats.get_profile_photos) def get_profile_photos( self: 'TelegramClient', - entity: 'hints.EntityLike', + profile: 'hints.EntityLike', limit: int = (), *, offset: int = 0, @@ -968,8 +968,8 @@ class TelegramClient: The order is from the most recent photo to the oldest. Arguments - entity (`entity`): - The entity from which to get the profile or chat photos. + profile (`entity`): + The user or chat profile from which to get the profile photos. limit (`int` | `None`, optional): Number of photos to be retrieved. @@ -1005,7 +1005,7 @@ class TelegramClient: @forward_call(chats.action) def action( self: 'TelegramClient', - entity: 'hints.EntityLike', + dialog: 'hints.EntityLike', action: 'typing.Union[str, _tl.TypeSendMessageAction]', *, delay: float = 4, @@ -1022,8 +1022,8 @@ class TelegramClient: See the example below for intended usage. Arguments - entity (`entity`): - The entity where the action should be showed in. + dialog (`entity`): + The dialog where the action should be showed in. action (`str` | :tl:`SendMessageAction`): The action to show. You can either pass a instance of @@ -1083,7 +1083,7 @@ class TelegramClient: @forward_call(chats.edit_admin) async def edit_admin( self: 'TelegramClient', - entity: 'hints.EntityLike', + chat: 'hints.EntityLike', user: 'hints.EntityLike', *, change_info: bool = None, @@ -1107,8 +1107,8 @@ class TelegramClient: Unless otherwise stated, permissions will work in channels and megagroups. Arguments - entity (`entity`): - The channel, megagroup or chat where the promotion should happen. + chat (`entity`): + The chat where the promotion should happen. user (`entity`): The user to be promoted. @@ -1189,7 +1189,7 @@ class TelegramClient: @forward_call(chats.edit_permissions) async def edit_permissions( self: 'TelegramClient', - entity: 'hints.EntityLike', + chat: 'hints.EntityLike', user: 'typing.Optional[hints.EntityLike]' = None, until_date: 'hints.DateLike' = None, *, @@ -1229,8 +1229,8 @@ class TelegramClient: permissions don't allow it either. Arguments - entity (`entity`): - The channel or megagroup where the restriction should happen. + chat (`entity`): + The chat where the restriction should happen. user (`entity`, optional): If specified, the permission will be changed for the specific user. @@ -1306,7 +1306,7 @@ class TelegramClient: @forward_call(chats.kick_participant) async def kick_participant( self: 'TelegramClient', - entity: 'hints.EntityLike', + chat: 'hints.EntityLike', user: 'typing.Optional[hints.EntityLike]' ): """ @@ -1321,8 +1321,8 @@ class TelegramClient: ban + unban. Arguments - entity (`entity`): - The channel or chat where the user should be kicked from. + chat (`entity`): + The chat where the user should be kicked from. user (`entity`, optional): The user to kick. @@ -1345,7 +1345,7 @@ class TelegramClient: @forward_call(chats.get_permissions) async def get_permissions( self: 'TelegramClient', - entity: 'hints.EntityLike', + chat: 'hints.EntityLike', user: 'hints.EntityLike' = None ) -> 'typing.Optional[_custom.ParticipantPermissions]': """ @@ -1358,8 +1358,8 @@ class TelegramClient: which can get somewhat expensive, so use of a cache is advised. Arguments - entity (`entity`): - The channel or chat the user is participant of. + chat (`entity`): + The chat the user is participant of. user (`entity`, optional): Target user. @@ -1383,7 +1383,7 @@ class TelegramClient: @forward_call(chats.get_stats) async def get_stats( self: 'TelegramClient', - entity: 'hints.EntityLike', + chat: 'hints.EntityLike', message: 'typing.Union[int, _tl.Message]' = None, ): """ @@ -1394,22 +1394,22 @@ class TelegramClient: requires `at least 500 members`_). Arguments - entity (`entity`): - The channel from which to get statistics. + chat (`entity`): + The chat from which to get statistics. message (`int` | ``Message``, optional): The message ID from which to get statistics, if your goal is to obtain the statistics of a single message. Raises - If the given entity is not a channel (broadcast or megagroup), + If the given chat is not a broadcast channel ormegagroup, a `TypeError` is raised. If there are not enough members (poorly named) errors such as ``telethon.errors.ChatAdminRequiredError`` will appear. Returns - If both ``entity`` and ``message`` were provided, returns + If both ``chat`` and ``message`` were provided, returns :tl:`MessageStats`. Otherwise, either :tl:`BroadcastStats` or :tl:`MegagroupStats`, depending on whether the input belonged to a broadcast channel or megagroup. @@ -1523,7 +1523,7 @@ class TelegramClient: @forward_call(dialogs.get_drafts) def get_drafts( self: 'TelegramClient', - entity: 'hints.EntitiesLike' = None + dialog: 'hints.EntitiesLike' = None ) -> dialogs._DraftsIter: """ Iterator over draft messages. @@ -1531,8 +1531,8 @@ class TelegramClient: The order is unspecified. Arguments - entity (`hints.EntitiesLike`, optional): - The entity or entities for which to fetch the draft messages. + dialog (`hints.EntitiesLike`, optional): + The dialog or dialogs for which to fetch the draft messages. If left unspecified, all draft messages will be returned. Yields @@ -1557,7 +1557,7 @@ class TelegramClient: @forward_call(dialogs.delete_dialog) async def delete_dialog( self: 'TelegramClient', - entity: 'hints.EntityLike', + dialog: 'hints.EntityLike', *, revoke: bool = False ): @@ -1571,8 +1571,8 @@ class TelegramClient: See also `Dialog.delete() `. Arguments - entity (entities): - The entity of the dialog to delete. If it's a chat or + dialog (entities): + 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. @@ -1606,7 +1606,7 @@ class TelegramClient: @forward_call(downloads.download_profile_photo) async def download_profile_photo( self: 'TelegramClient', - entity: 'hints.EntityLike', + profile: 'hints.EntityLike', file: 'hints.FileLike' = None, *, thumb: typing.Union[str, enums.Size] = (), @@ -1615,8 +1615,8 @@ class TelegramClient: Downloads the profile photo from the given user, chat or channel. Arguments - entity (`entity`): - From who the photo will be downloaded. + profile (`entity`): + The profile from which to download its photo. .. note:: @@ -1663,7 +1663,7 @@ class TelegramClient: @forward_call(downloads.download_media) async def download_media( self: 'TelegramClient', - message: 'hints.MessageLike', + media: 'hints.MessageLike', file: 'hints.FileLike' = None, *, thumb: typing.Union[str, enums.Size] = (), @@ -1678,8 +1678,8 @@ class TelegramClient: See also `Message.download_media() `. Arguments - message (`Message ` | :tl:`Media`): - The media or message containing the media that will be downloaded. + media (:tl:`Media`): + The media that will be downloaded. file (`str` | `file`, optional): The output file path, directory, or stream-like object. @@ -1725,7 +1725,7 @@ class TelegramClient: @forward_call(downloads.iter_download) def iter_download( self: 'TelegramClient', - file: 'hints.FileLike', + media: 'hints.FileLike', *, offset: int = 0, stride: int = None, @@ -1826,7 +1826,7 @@ class TelegramClient: @forward_call(messages.get_messages) def get_messages( self: 'TelegramClient', - entity: 'hints.EntityLike', + dialog: 'hints.EntityLike', limit: float = (), *, offset_date: 'hints.DateLike' = None, @@ -1859,8 +1859,8 @@ class TelegramClient: second is the default for this limit (or above). Arguments - entity (`entity`): - The entity from whom to retrieve the message history. + dialog (`entity`): + The dialog from which to retrieve the message history. It may be `None` to perform a global search, or to get messages by their ID from no particular chat. @@ -1912,7 +1912,7 @@ class TelegramClient: containing photos. from_user (`entity`): - Only messages from this entity will be returned. + Only messages from this user will be returned. wait_time (`int`): Wait time (in seconds) between different @@ -1950,7 +1950,7 @@ class TelegramClient: instead of being `max_id` as well since messages are returned in ascending order. - You cannot use this if both `entity` and `ids` are `None`. + You cannot use this if both `dialog` and `ids` are `None`. reply_to (`int`, optional): If set to a message ID, the messages that reply to this ID @@ -1974,7 +1974,7 @@ class TelegramClient: scheduled (`bool`, optional): If set to `True`, messages which are scheduled will be returned. - All other parameter will be ignored for this, except `entity`. + All other parameter will be ignored for this, except `dialog`. Yields Instances of `Message `. @@ -2025,7 +2025,7 @@ class TelegramClient: @forward_call(messages.send_message) async def send_message( self: 'TelegramClient', - entity: 'hints.EntityLike', + dialog: 'hints.EntityLike', message: 'hints.MessageLike' = '', *, # - Message contents @@ -2077,7 +2077,7 @@ class TelegramClient: and `Message.reply() `. Arguments - entity (`entity`): + dialog (`entity`): To who will it be sent. message (`str` | `Message `): @@ -2235,9 +2235,9 @@ class TelegramClient: @forward_call(messages.forward_messages) async def forward_messages( self: 'TelegramClient', - entity: 'hints.EntityLike', + dialog: 'hints.EntityLike', messages: 'typing.Union[typing.Sequence[hints.MessageIDLike]]', - from_peer: 'hints.EntityLike' = None, + from_dialog: 'hints.EntityLike' = None, *, background: bool = None, with_my_score: bool = None, @@ -2246,7 +2246,7 @@ class TelegramClient: schedule: 'hints.DateLike' = None ) -> 'typing.Sequence[_tl.Message]': """ - Forwards the given messages to the specified entity. + Forwards the given messages to the specified dialog. If you want to "forward" a message without the forward header (the "forwarded from" text), you should use `send_message` with @@ -2255,17 +2255,17 @@ class TelegramClient: See also `Message.forward_to() `. Arguments - entity (`entity`): - To which entity the message(s) will be forwarded. + dialog (`entity`): + The target dialog where the message(s) will be forwarded. messages (`list`): The messages to forward, or their integer IDs. - from_peer (`entity`): + from_dialog (`entity`): If the given messages are integer IDs and not instances of the ``Message`` class, this *must* be specified in order for the forward to work. This parameter indicates - the entity from which the messages should be forwarded. + the source dialog from which the messages should be forwarded. silent (`bool`, optional): Whether the message should notify people with sound or not. @@ -2317,7 +2317,7 @@ class TelegramClient: @forward_call(messages.edit_message) async def edit_message( self: 'TelegramClient', - entity: 'typing.Union[hints.EntityLike, _tl.Message]', + dialog: 'typing.Union[hints.EntityLike, _tl.Message]', message: 'hints.MessageLike', text: str = None, *, @@ -2338,9 +2338,9 @@ class TelegramClient: See also `Message.edit() `. Arguments - entity (`entity` | `Message `): + dialog (`entity` | `Message `): From which chat to edit the message. This can also be - the message to be edited, and the entity will be inferred + the message to be edited, and the dialog will be inferred from it, so the next parameter will be assumed to be the message text. @@ -2351,12 +2351,12 @@ class TelegramClient: message (`int` | `Message ` | `str`): The ID of the message (or `Message ` itself) to be edited. - If the `entity` was a `Message + If the `dialog` was a `Message `, then this message will be treated as the new text. text (`str`, optional): - The new text of the message. Does nothing if the `entity` + The new text of the message. Does nothing if the `dialog` was a `Message `. parse_mode (`object`, optional): @@ -2416,7 +2416,7 @@ class TelegramClient: Returns The edited `Message `, - unless `entity` was a :tl:`InputBotInlineMessageID` in which + unless `dialog` was a :tl:`InputBotInlineMessageID` in which case this method returns a boolean. Raises @@ -2446,7 +2446,7 @@ class TelegramClient: @forward_call(messages.delete_messages) async def delete_messages( self: 'TelegramClient', - entity: 'hints.EntityLike', + dialog: 'hints.EntityLike', messages: 'typing.Union[typing.Sequence[hints.MessageIDLike]]', *, revoke: bool = True) -> 'typing.Sequence[_tl.messages.AffectedMessages]': @@ -2463,7 +2463,7 @@ class TelegramClient: chats at once, so make sure to pass the right IDs. Arguments - entity (`entity`): + dialog (`entity`): From who the message will be deleted. This can actually be `None` for normal chats, but **must** be present for channels and megagroups. @@ -2498,7 +2498,7 @@ class TelegramClient: @forward_call(messages.mark_read) async def mark_read( self: 'TelegramClient', - entity: 'hints.EntityLike', + dialog: 'hints.EntityLike', message: 'hints.MessageIDLike' = None, *, clear_mentions: bool = False) -> bool: @@ -2518,7 +2518,7 @@ class TelegramClient: See also `Message.mark_read() `. Arguments - entity (`entity`): + dialog (`entity`): The chat where these messages are located. message (`Message `): @@ -2550,7 +2550,7 @@ class TelegramClient: @forward_call(messages.pin_message) async def pin_message( self: 'TelegramClient', - entity: 'hints.EntityLike', + dialog: 'hints.EntityLike', message: 'typing.Optional[hints.MessageIDLike]', *, notify: bool = False, @@ -2565,7 +2565,7 @@ class TelegramClient: See also `Message.pin() `. Arguments - entity (`entity`): + dialog (`entity`): The chat where the message should be pinned. message (`int` | `Message `): @@ -2591,7 +2591,7 @@ class TelegramClient: @forward_call(messages.unpin_message) async def unpin_message( self: 'TelegramClient', - entity: 'hints.EntityLike', + dialog: 'hints.EntityLike', message: 'typing.Optional[hints.MessageIDLike]' = None, *, notify: bool = False @@ -2604,8 +2604,8 @@ class TelegramClient: See also `Message.unpin() `. Arguments - entity (`entity`): - The chat where the message should be pinned. + dialog (`entity`): + The dialog where the message should be pinned. message (`int` | `Message `): The message or the message ID to unpin. If it's @@ -2968,7 +2968,7 @@ class TelegramClient: @forward_call(uploads.send_file) async def send_file( self: 'TelegramClient', - entity: 'hints.EntityLike', + dialog: 'hints.EntityLike', file: 'typing.Union[hints.FileLike, typing.Sequence[hints.FileLike]]', *, caption: typing.Union[str, typing.Sequence[str]] = None, @@ -3006,7 +3006,7 @@ class TelegramClient: cannot be done if you are sending :tl:`InputFile`, however. Arguments - entity (`entity`): + dialog (`entity`): Who will receive the file. file (`str` | `bytes` | `file` | `media`): diff --git a/telethon/_client/uploads.py b/telethon/_client/uploads.py index 218e6351..75b1ed12 100644 --- a/telethon/_client/uploads.py +++ b/telethon/_client/uploads.py @@ -79,7 +79,7 @@ def _resize_photo_if_needed( async def send_file( self: 'TelegramClient', - entity: 'hints.EntityLike', + dialog: 'hints.EntityLike', file: typing.Optional[hints.FileLike] = None, *, # - Message contents @@ -119,7 +119,7 @@ async def send_file( comment_to: 'typing.Union[int, _tl.Message]' = None, ) -> '_tl.Message': self.send_message( - entity=entity, + dialog=dialog, message=caption, markdown=markdown, html=html, From 68d07beb215e2cfdfad801f337ed55dcc0a6d3f5 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 2 Mar 2022 12:38:59 +0100 Subject: [PATCH 242/256] Rename Entity type hint --- telethon/_client/bots.py | 4 +- telethon/_client/chats.py | 28 ++++++------- telethon/_client/dialogs.py | 6 +-- telethon/_client/downloads.py | 2 +- telethon/_client/messages.py | 30 +++++++------- telethon/_client/telegramclient.py | 66 +++++++++++++++--------------- telethon/_client/uploads.py | 4 +- telethon/_misc/hints.py | 12 +++--- telethon/types/_custom/button.py | 2 +- 9 files changed, 77 insertions(+), 77 deletions(-) diff --git a/telethon/_client/bots.py b/telethon/_client/bots.py index a64fe810..22945cbf 100644 --- a/telethon/_client/bots.py +++ b/telethon/_client/bots.py @@ -10,10 +10,10 @@ if typing.TYPE_CHECKING: async def inline_query( self: 'TelegramClient', - bot: 'hints.EntityLike', + bot: 'hints.DialogLike', query: str, *, - dialog: 'hints.EntityLike' = None, + dialog: 'hints.DialogLike' = None, offset: str = None, geo_point: '_tl.GeoPoint' = None) -> _custom.InlineResults: bot = await self.get_input_entity(bot) diff --git a/telethon/_client/chats.py b/telethon/_client/chats.py index f45e9bf5..2bb34d45 100644 --- a/telethon/_client/chats.py +++ b/telethon/_client/chats.py @@ -372,7 +372,7 @@ class _ProfilePhotoIter(requestiter.RequestIter): def get_participants( self: 'TelegramClient', - chat: 'hints.EntityLike', + chat: 'hints.DialogLike', limit: float = (), *, search: str = '', @@ -388,13 +388,13 @@ def get_participants( def get_admin_log( self: 'TelegramClient', - chat: 'hints.EntityLike', + chat: 'hints.DialogLike', limit: float = (), *, max_id: int = 0, min_id: int = 0, search: str = None, - admins: 'hints.EntitiesLike' = None, + admins: 'hints.DialogsLike' = None, join: bool = None, leave: bool = None, invite: bool = None, @@ -438,7 +438,7 @@ def get_admin_log( def get_profile_photos( self: 'TelegramClient', - profile: 'hints.EntityLike', + profile: 'hints.DialogLike', limit: int = (), *, offset: int = 0, @@ -454,7 +454,7 @@ def get_profile_photos( def action( self: 'TelegramClient', - dialog: 'hints.EntityLike', + dialog: 'hints.DialogLike', action: 'typing.Union[str, _tl.TypeSendMessageAction]', *, delay: float = 4, @@ -466,8 +466,8 @@ def action( async def edit_admin( self: 'TelegramClient', - chat: 'hints.EntityLike', - user: 'hints.EntityLike', + chat: 'hints.DialogLike', + user: 'hints.DialogLike', *, change_info: bool = None, post_messages: bool = None, @@ -529,8 +529,8 @@ async def edit_admin( async def edit_permissions( self: 'TelegramClient', - chat: 'hints.EntityLike', - user: 'typing.Optional[hints.EntityLike]' = None, + chat: 'hints.DialogLike', + user: 'typing.Optional[hints.DialogLike]' = None, until_date: 'hints.DateLike' = None, *, view_messages: bool = True, @@ -583,8 +583,8 @@ async def edit_permissions( async def kick_participant( self: 'TelegramClient', - chat: 'hints.EntityLike', - user: 'typing.Optional[hints.EntityLike]' + chat: 'hints.DialogLike', + user: 'typing.Optional[hints.DialogLike]' ): entity = await self.get_input_entity(chat) user = await self.get_input_entity(user) @@ -617,8 +617,8 @@ async def kick_participant( async def get_permissions( self: 'TelegramClient', - chat: 'hints.EntityLike', - user: 'hints.EntityLike' = None + chat: 'hints.DialogLike', + user: 'hints.DialogLike' = None ) -> 'typing.Optional[_custom.ParticipantPermissions]': entity = await self.get_entity(chat) @@ -650,7 +650,7 @@ async def get_permissions( async def get_stats( self: 'TelegramClient', - chat: 'hints.EntityLike', + chat: 'hints.DialogLike', message: 'typing.Union[int, _tl.Message]' = None, ): entity = await self.get_input_entity(chat) diff --git a/telethon/_client/dialogs.py b/telethon/_client/dialogs.py index 8151f5dc..c1d12028 100644 --- a/telethon/_client/dialogs.py +++ b/telethon/_client/dialogs.py @@ -146,7 +146,7 @@ def get_dialogs( *, offset_date: 'hints.DateLike' = None, offset_id: int = 0, - offset_peer: 'hints.EntityLike' = _tl.InputPeerEmpty(), + offset_peer: 'hints.DialogLike' = _tl.InputPeerEmpty(), ignore_pinned: bool = False, ignore_migrated: bool = False, folder: int = None, @@ -165,7 +165,7 @@ def get_dialogs( def get_drafts( self: 'TelegramClient', - dialog: 'hints.EntitiesLike' = None + dialog: 'hints.DialogsLike' = None ) -> _DraftsIter: limit = None if dialog: @@ -178,7 +178,7 @@ def get_drafts( async def delete_dialog( self: 'TelegramClient', - dialog: 'hints.EntityLike', + dialog: 'hints.DialogLike', *, revoke: bool = False ): diff --git a/telethon/_client/downloads.py b/telethon/_client/downloads.py index 35f1608e..f73b1412 100644 --- a/telethon/_client/downloads.py +++ b/telethon/_client/downloads.py @@ -178,7 +178,7 @@ class _GenericDownloadIter(_DirectDownloadIter): async def download_profile_photo( self: 'TelegramClient', - profile: 'hints.EntityLike', + profile: 'hints.DialogLike', file: 'hints.FileLike' = None, *, thumb, diff --git a/telethon/_client/messages.py b/telethon/_client/messages.py index da8fbf5c..f5ab9ff3 100644 --- a/telethon/_client/messages.py +++ b/telethon/_client/messages.py @@ -322,7 +322,7 @@ class _IDsIter(requestiter.RequestIter): self.buffer.append(_custom.Message._new(self.client, message, entities, self._entity)) -async def _get_peer(self: 'TelegramClient', input_peer: 'hints.EntityLike'): +async def _get_peer(self: 'TelegramClient', input_peer: 'hints.DialogLike'): try: return utils.get_peer(input_peer) except TypeError: @@ -332,7 +332,7 @@ async def _get_peer(self: 'TelegramClient', input_peer: 'hints.EntityLike'): def get_messages( self: 'TelegramClient', - dialog: 'hints.EntityLike', + dialog: 'hints.DialogLike', limit: float = (), *, offset_date: 'hints.DateLike' = None, @@ -342,7 +342,7 @@ def get_messages( add_offset: int = 0, search: str = None, filter: 'typing.Union[_tl.TypeMessagesFilter, typing.Type[_tl.TypeMessagesFilter]]' = None, - from_user: 'hints.EntityLike' = None, + from_user: 'hints.DialogLike' = None, wait_time: float = None, ids: 'typing.Union[int, typing.Sequence[int]]' = None, reverse: bool = False, @@ -383,7 +383,7 @@ def get_messages( async def _get_comment_data( self: 'TelegramClient', - entity: 'hints.EntityLike', + entity: 'hints.DialogLike', message: 'typing.Union[int, _tl.Message]' ): r = await self(_tl.fn.messages.GetDiscussionMessage( @@ -396,7 +396,7 @@ async def _get_comment_data( async def send_message( self: 'TelegramClient', - dialog: 'hints.EntityLike', + dialog: 'hints.DialogLike', message: 'hints.MessageLike' = '', *, # - Message contents @@ -428,7 +428,7 @@ async def send_message( ttl: int = None, # - Send options reply_to: 'typing.Union[int, _tl.Message]' = None, - send_as: 'hints.EntityLike' = None, + send_as: 'hints.DialogLike' = None, clear_draft: bool = False, background: bool = None, noforwards: bool = None, @@ -525,9 +525,9 @@ async def send_message( async def forward_messages( self: 'TelegramClient', - dialog: 'hints.EntityLike', + dialog: 'hints.DialogLike', messages: 'typing.Union[typing.Sequence[hints.MessageIDLike]]', - from_dialog: 'hints.EntityLike' = None, + from_dialog: 'hints.DialogLike' = None, *, background: bool = None, with_my_score: bool = None, @@ -535,7 +535,7 @@ async def forward_messages( as_album: bool = None, schedule: 'hints.DateLike' = None, noforwards: bool = None, - send_as: 'hints.EntityLike' = None + send_as: 'hints.DialogLike' = None ) -> 'typing.Sequence[_tl.Message]': if as_album is not None: warnings.warn('the as_album argument is deprecated and no longer has any effect') @@ -588,7 +588,7 @@ async def forward_messages( async def edit_message( self: 'TelegramClient', - dialog: 'typing.Union[hints.EntityLike, _tl.Message]', + dialog: 'typing.Union[hints.DialogLike, _tl.Message]', message: 'hints.MessageLike' = None, text: str = None, *, @@ -648,7 +648,7 @@ async def edit_message( async def delete_messages( self: 'TelegramClient', - dialog: 'hints.EntityLike', + dialog: 'hints.DialogLike', messages: 'typing.Union[typing.Sequence[hints.MessageIDLike]]', *, revoke: bool = True) -> 'typing.Sequence[_tl.messages.AffectedMessages]': @@ -677,7 +677,7 @@ async def delete_messages( async def mark_read( self: 'TelegramClient', - dialog: 'hints.EntityLike', + dialog: 'hints.DialogLike', message: 'hints.MessageIDLike' = None, *, clear_mentions: bool = False, @@ -707,7 +707,7 @@ async def mark_read( async def pin_message( self: 'TelegramClient', - dialog: 'hints.EntityLike', + dialog: 'hints.DialogLike', message: 'typing.Optional[hints.MessageIDLike]', *, notify: bool = False, @@ -717,7 +717,7 @@ async def pin_message( async def unpin_message( self: 'TelegramClient', - dialog: 'hints.EntityLike', + dialog: 'hints.DialogLike', message: 'typing.Optional[hints.MessageIDLike]' = None, *, notify: bool = False @@ -752,7 +752,7 @@ async def _pin(self, entity, message, *, unpin, notify=False, pm_oneside=False): async def send_reaction( self: 'TelegramClient', - entity: 'hints.EntityLike', + entity: 'hints.DialogLike', message: 'hints.MessageIDLike', reaction: typing.Optional[str] = None, big: bool = False diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 7a9f767f..28e89531 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -701,10 +701,10 @@ class TelegramClient: @forward_call(bots.inline_query) async def inline_query( self: 'TelegramClient', - bot: 'hints.EntityLike', + bot: 'hints.DialogLike', query: str, *, - dialog: 'hints.EntityLike' = None, + dialog: 'hints.DialogLike' = None, offset: str = None, geo_point: '_tl.GeoPoint' = None) -> _custom.InlineResults: """ @@ -754,7 +754,7 @@ class TelegramClient: @forward_call(chats.get_participants) def get_participants( self: 'TelegramClient', - chat: 'hints.EntityLike', + chat: 'hints.DialogLike', limit: float = (), *, search: str = '', @@ -822,13 +822,13 @@ class TelegramClient: @forward_call(chats.get_admin_log) def get_admin_log( self: 'TelegramClient', - chat: 'hints.EntityLike', + chat: 'hints.DialogLike', limit: float = (), *, max_id: int = 0, min_id: int = 0, search: str = None, - admins: 'hints.EntitiesLike' = None, + admins: 'hints.DialogsLike' = None, join: bool = None, leave: bool = None, invite: bool = None, @@ -957,7 +957,7 @@ class TelegramClient: @forward_call(chats.get_profile_photos) def get_profile_photos( self: 'TelegramClient', - profile: 'hints.EntityLike', + profile: 'hints.DialogLike', limit: int = (), *, offset: int = 0, @@ -1005,7 +1005,7 @@ class TelegramClient: @forward_call(chats.action) def action( self: 'TelegramClient', - dialog: 'hints.EntityLike', + dialog: 'hints.DialogLike', action: 'typing.Union[str, _tl.TypeSendMessageAction]', *, delay: float = 4, @@ -1083,8 +1083,8 @@ class TelegramClient: @forward_call(chats.edit_admin) async def edit_admin( self: 'TelegramClient', - chat: 'hints.EntityLike', - user: 'hints.EntityLike', + chat: 'hints.DialogLike', + user: 'hints.DialogLike', *, change_info: bool = None, post_messages: bool = None, @@ -1189,8 +1189,8 @@ class TelegramClient: @forward_call(chats.edit_permissions) async def edit_permissions( self: 'TelegramClient', - chat: 'hints.EntityLike', - user: 'typing.Optional[hints.EntityLike]' = None, + chat: 'hints.DialogLike', + user: 'typing.Optional[hints.DialogLike]' = None, until_date: 'hints.DateLike' = None, *, view_messages: bool = True, @@ -1306,8 +1306,8 @@ class TelegramClient: @forward_call(chats.kick_participant) async def kick_participant( self: 'TelegramClient', - chat: 'hints.EntityLike', - user: 'typing.Optional[hints.EntityLike]' + chat: 'hints.DialogLike', + user: 'typing.Optional[hints.DialogLike]' ): """ Kicks a user from a chat. @@ -1345,8 +1345,8 @@ class TelegramClient: @forward_call(chats.get_permissions) async def get_permissions( self: 'TelegramClient', - chat: 'hints.EntityLike', - user: 'hints.EntityLike' = None + chat: 'hints.DialogLike', + user: 'hints.DialogLike' = None ) -> 'typing.Optional[_custom.ParticipantPermissions]': """ Fetches the permissions of a user in a specific chat or channel or @@ -1383,7 +1383,7 @@ class TelegramClient: @forward_call(chats.get_stats) async def get_stats( self: 'TelegramClient', - chat: 'hints.EntityLike', + chat: 'hints.DialogLike', message: 'typing.Union[int, _tl.Message]' = None, ): """ @@ -1437,7 +1437,7 @@ class TelegramClient: *, offset_date: 'hints.DateLike' = None, offset_id: int = 0, - offset_peer: 'hints.EntityLike' = _tl.InputPeerEmpty(), + offset_peer: 'hints.DialogLike' = _tl.InputPeerEmpty(), ignore_pinned: bool = False, ignore_migrated: bool = False, folder: int = None, @@ -1523,7 +1523,7 @@ class TelegramClient: @forward_call(dialogs.get_drafts) def get_drafts( self: 'TelegramClient', - dialog: 'hints.EntitiesLike' = None + dialog: 'hints.DialogsLike' = None ) -> dialogs._DraftsIter: """ Iterator over draft messages. @@ -1531,7 +1531,7 @@ class TelegramClient: The order is unspecified. Arguments - dialog (`hints.EntitiesLike`, optional): + dialog (`hints.DialogsLike`, optional): The dialog or dialogs for which to fetch the draft messages. If left unspecified, all draft messages will be returned. @@ -1557,7 +1557,7 @@ class TelegramClient: @forward_call(dialogs.delete_dialog) async def delete_dialog( self: 'TelegramClient', - dialog: 'hints.EntityLike', + dialog: 'hints.DialogLike', *, revoke: bool = False ): @@ -1606,7 +1606,7 @@ class TelegramClient: @forward_call(downloads.download_profile_photo) async def download_profile_photo( self: 'TelegramClient', - profile: 'hints.EntityLike', + profile: 'hints.DialogLike', file: 'hints.FileLike' = None, *, thumb: typing.Union[str, enums.Size] = (), @@ -1826,7 +1826,7 @@ class TelegramClient: @forward_call(messages.get_messages) def get_messages( self: 'TelegramClient', - dialog: 'hints.EntityLike', + dialog: 'hints.DialogLike', limit: float = (), *, offset_date: 'hints.DateLike' = None, @@ -1836,7 +1836,7 @@ class TelegramClient: add_offset: int = 0, search: str = None, filter: 'typing.Union[_tl.TypeMessagesFilter, typing.Type[_tl.TypeMessagesFilter]]' = None, - from_user: 'hints.EntityLike' = None, + from_user: 'hints.DialogLike' = None, wait_time: float = None, ids: 'typing.Union[int, typing.Sequence[int]]' = None, reverse: bool = False, @@ -2025,7 +2025,7 @@ class TelegramClient: @forward_call(messages.send_message) async def send_message( self: 'TelegramClient', - dialog: 'hints.EntityLike', + dialog: 'hints.DialogLike', message: 'hints.MessageLike' = '', *, # - Message contents @@ -2235,9 +2235,9 @@ class TelegramClient: @forward_call(messages.forward_messages) async def forward_messages( self: 'TelegramClient', - dialog: 'hints.EntityLike', + dialog: 'hints.DialogLike', messages: 'typing.Union[typing.Sequence[hints.MessageIDLike]]', - from_dialog: 'hints.EntityLike' = None, + from_dialog: 'hints.DialogLike' = None, *, background: bool = None, with_my_score: bool = None, @@ -2317,7 +2317,7 @@ class TelegramClient: @forward_call(messages.edit_message) async def edit_message( self: 'TelegramClient', - dialog: 'typing.Union[hints.EntityLike, _tl.Message]', + dialog: 'typing.Union[hints.DialogLike, _tl.Message]', message: 'hints.MessageLike', text: str = None, *, @@ -2446,7 +2446,7 @@ class TelegramClient: @forward_call(messages.delete_messages) async def delete_messages( self: 'TelegramClient', - dialog: 'hints.EntityLike', + dialog: 'hints.DialogLike', messages: 'typing.Union[typing.Sequence[hints.MessageIDLike]]', *, revoke: bool = True) -> 'typing.Sequence[_tl.messages.AffectedMessages]': @@ -2498,7 +2498,7 @@ class TelegramClient: @forward_call(messages.mark_read) async def mark_read( self: 'TelegramClient', - dialog: 'hints.EntityLike', + dialog: 'hints.DialogLike', message: 'hints.MessageIDLike' = None, *, clear_mentions: bool = False) -> bool: @@ -2550,7 +2550,7 @@ class TelegramClient: @forward_call(messages.pin_message) async def pin_message( self: 'TelegramClient', - dialog: 'hints.EntityLike', + dialog: 'hints.DialogLike', message: 'typing.Optional[hints.MessageIDLike]', *, notify: bool = False, @@ -2591,7 +2591,7 @@ class TelegramClient: @forward_call(messages.unpin_message) async def unpin_message( self: 'TelegramClient', - dialog: 'hints.EntityLike', + dialog: 'hints.DialogLike', message: 'typing.Optional[hints.MessageIDLike]' = None, *, notify: bool = False @@ -2968,7 +2968,7 @@ class TelegramClient: @forward_call(uploads.send_file) async def send_file( self: 'TelegramClient', - dialog: 'hints.EntityLike', + dialog: 'hints.DialogLike', file: 'typing.Union[hints.FileLike, typing.Sequence[hints.FileLike]]', *, caption: typing.Union[str, typing.Sequence[str]] = None, @@ -3439,7 +3439,7 @@ class TelegramClient: @forward_call(messages._get_comment_data) async def _get_comment_data( self: 'TelegramClient', - entity: 'hints.EntityLike', + entity: 'hints.DialogLike', message: 'typing.Union[int, _tl.Message]' ): pass diff --git a/telethon/_client/uploads.py b/telethon/_client/uploads.py index 75b1ed12..c91acdcb 100644 --- a/telethon/_client/uploads.py +++ b/telethon/_client/uploads.py @@ -79,7 +79,7 @@ def _resize_photo_if_needed( async def send_file( self: 'TelegramClient', - dialog: 'hints.EntityLike', + dialog: 'hints.DialogLike', file: typing.Optional[hints.FileLike] = None, *, # - Message contents @@ -114,7 +114,7 @@ async def send_file( clear_draft: bool = False, background: bool = None, noforwards: bool = None, - send_as: 'hints.EntityLike' = None, + send_as: 'hints.DialogLike' = None, schedule: 'hints.DateLike' = None, comment_to: 'typing.Union[int, _tl.Message]' = None, ) -> '_tl.Message': diff --git a/telethon/_misc/hints.py b/telethon/_misc/hints.py index 52951cec..b97cbc44 100644 --- a/telethon/_misc/hints.py +++ b/telethon/_misc/hints.py @@ -8,19 +8,19 @@ from ..types import _custom Phone = str Username = str PeerID = int -Entity = typing.Union[_tl.User, _tl.Chat, _tl.Channel] -FullEntity = typing.Union[_tl.UserFull, _tl.messages.ChatFull, _tl.ChatFull, _tl.ChannelFull] +Dialog = typing.Union[_tl.User, _tl.Chat, _tl.Channel] +FullDialog = typing.Union[_tl.UserFull, _tl.messages.ChatFull, _tl.ChatFull, _tl.ChannelFull] -EntityLike = typing.Union[ +DialogLike = typing.Union[ Phone, Username, PeerID, _tl.TypePeer, _tl.TypeInputPeer, - Entity, - FullEntity + Dialog, + FullDialog ] -EntitiesLike = typing.Union[EntityLike, typing.Sequence[EntityLike]] +DialogsLike = typing.Union[DialogLike, typing.Sequence[DialogsLike]] ButtonLike = typing.Union[_tl.TypeKeyboardButton, _custom.Button] MarkupLike = typing.Union[ diff --git a/telethon/types/_custom/button.py b/telethon/types/_custom/button.py index 34897110..243f13be 100644 --- a/telethon/types/_custom/button.py +++ b/telethon/types/_custom/button.py @@ -138,7 +138,7 @@ class Button: If no `url` is specified, it will default to `text`. Args: - bot (`hints.EntityLike`): + bot (`hints.DialogLike`): The bot that requires this authorization. By default, this is the bot that is currently logged in (itself), although you may pass a different input peer. From 3865b7fa53635605b7ba0910ef5205b11268a961 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 2 Mar 2022 12:39:47 +0100 Subject: [PATCH 243/256] Rename get_entity to get_profile --- readthedocs/concepts/entities.rst | 390 ++++++++++++------------ readthedocs/misc/v2-migration-guide.rst | 1 + telethon/_client/telegramclient.py | 38 +-- telethon/_client/users.py | 10 +- 4 files changed, 208 insertions(+), 231 deletions(-) diff --git a/readthedocs/concepts/entities.rst b/readthedocs/concepts/entities.rst index f76c3983..bd60072a 100644 --- a/readthedocs/concepts/entities.rst +++ b/readthedocs/concepts/entities.rst @@ -1,20 +1,27 @@ .. _entities: -======== -Entities -======== +=============== +Users and Chats +=============== -The library widely uses the concept of "entities". An entity will refer -to any :tl:`User`, :tl:`Chat` or :tl:`Channel` object that the API may return -in response to certain methods, such as :tl:`GetUsersRequest`. +The library widely uses the concept of "users" to refer to both real accounts +and bot accounts, as well as the concept of "chats" to refer to groups and +broadcast channels. + +The most general term you can use to think about these is "an entity", but +recent versions of the library often prefer to opt for names which better +reflect the intention, such as "dialog" when a previously-existing +conversation is expected, or "profile" when referring to the information about +the user or chat. .. note:: - When something "entity-like" is required, it means that you need to - provide something that can be turned into an entity. These things include, - but are not limited to, usernames, exact titles, IDs, :tl:`Peer` objects, - or even entire :tl:`User`, :tl:`Chat` and :tl:`Channel` objects and even - phone numbers **from people you have in your contact list**. + When something "dialog-like" is required, it means that you need to + provide something that can be used to refer to an open conversation. + These things include, but are not limited to, packed chats, usernames, + integer IDs (identifiers), :tl:`Peer` objects, or even entire :tl:`User`, + :tl:`Chat` and :tl:`Channel` objects and even phone numbers **from people + you have in your contact list**. To "encounter" an ID, you would have to "find it" like you would in the normal app. If the peer is in your dialogs, you would need to @@ -23,82 +30,123 @@ in response to certain methods, such as :tl:`GetUsersRequest`. `client.get_participants(group) `. Once you have encountered an ID, the library will (by default) have saved - their ``access_hash`` for you, which is needed to invoke most methods. + its packed version for you, which is needed to invoke most methods. This is why sometimes you might encounter this error when working with the library. You should ``except ValueError`` and run code that you know - should work to find the entity. + should work to find the user or chat. You **cannot** use an ID of someone + you haven't interacted with. Because this is more unreliable, packed chats + are recommended instead. .. contents:: -What is an Entity? -================== +What is a User? +=============== -A lot of methods and requests require *entities* to work. For example, -you send a message to an *entity*, get the username of an *entity*, and -so on. +A `User ` can be either a real user account +(some person who has signed up for an account) or a bot account which is +programmed to perform certain actions (created by a developer via +`@BotFather `_). -There are many things that work as entities: usernames, phone numbers, -chat links, invite links, IDs, and the types themselves. That is, you can -use any of those when you see an "entity" is needed. +A lot of methods and requests require user or chats to work. For example, +you can send a message to a *user*, ban a *user* from a group, and so on. +These methods accept more than just `User ` +as the input parameter. You can also use packed users, usernames, string phone +numbers, or integer IDs, although some have higher cost than others. + +When using the username, the library must fetch it first, which can be +expensive. When using the phone number, the library must fetch it first, which +can be expensive. If you plan to use these, it's recommended you manually use +`client.get_profile() ` to cache +the username or phone number, and then use the value returned instead. .. note:: Remember that the phone number must be in your contact list before you can use it. -You should use, **from better to worse**: +The recommended type to use as input parameters to the methods is either a +`User ` instance or its packed type. -1. Input entities. For example, `event.input_chat - `, - `message.input_sender - `, - or caching an entity you will use a lot with - ``entity = await client.get_input_entity(...)``. - -2. Entities. For example, if you had to get someone's - username, you can use ``user`` or ``channel``. - It will work. Only use this option if you already have the entity! - -3. IDs. This will always look the entity up from the - cache (the ``*.session`` file caches seen entities). - -4. Usernames, phone numbers, and links. The cache will be - used too (unless you force a `client.get_entity() - `), - but may make a request if the username, phone, or link - has not been found yet. - -In recent versions of the library, the following two are equivalent: - -.. code-block:: python - - async def handler(event): - await client.send_message(event.sender_id, 'Hi') - await client.send_message(event.input_sender, 'Hi') +In the raw API, users are instances of :tl:`User` (or :tl:`UserEmpty`), which +are returned in response to some requests, such as :tl:`GetUsersRequest`. +There are also variants for use as "input parameters", such as :tl:`InputUser` +and :tl:`InputPeerUser`. You generally **do not need** to worry about these +types unless you're using raw API. -If you need to be 99% sure that the code will work (sometimes it's -simply impossible for the library to find the input entity), or if -you will reuse the chat a lot, consider using the following instead: +What is a Chat? +=============== -.. code-block:: python +A `Chat ` can be a small group chat (the +default group type created by users where many users can join and talk), a +megagroup (also known as "supergroup"), a broadcast channel or a broadcast +group. - async def handler(event): - # This method may make a network request to find the input sender. - # Properties can't make network requests, so we need a method. - sender = await event.get_input_sender() - await client.send_message(sender, 'Hi') - await client.send_message(sender, 'Hi') +The term "chat" is really overloaded in Telegram. The library tries to be +explicit and always use "small group chat", "megagroup" and "broadcast" to +differentiate. However, Telegram's API uses "chat" to refer to both "chat" +(small group chat), and "channel" (megagroup, broadcast or "gigagroup" which +is a broadcast group of type channel). + +A lot of methods and requests require a chat to work. For example, +you can get the participants from a *chat*, kick users from a *chat*, and so on. +These methods accept more than just `Chat ` +as the input parameter. You can also use packed chats, the public link, or +integer IDs, although some have higher cost than others. + +When using the public link, the library must fetch it first, which can be +expensive. If you plan to use these, it's recommended you manually use +`client.get_profile() ` to cache +the link, and then use the value returned instead. + +.. note:: + + The link of a public chat has the form "t.me/username", where the username + can belong to either an actual user or a public chat. + +The recommended type to use as input parameters to the methods is either a +`Chat ` instance or its packed type. + +In the raw API, chats are instances of :tl:`Chat` and :tl:`Channel` (or +:tl:`ChatEmpty`, :tl:`ChatForbidden` and :tl:`ChannelForbidden`), which +are returned in response to some requests, such as :tl:`messages.GetChats` +and :tl:`channels.GetChannels`. There are also variants for use as "input +parameters", such as :tl:`InputChannel` and :tl:`InputPeerChannel`. You +generally **do not need** to worry about these types unless you're using raw API. -Getting Entities -================ +When to use each term? +====================== + +The term "dialog" is used when the library expects a reference to an open +conversation (from the list the user sees when they open the application). + +The term "profile" is used instead of "dialog" when the conversation is not +expected to exist. Because "dialog" is more specific than "profile", "dialog" +is used where possible instead. + +In general, you should not use named arguments for neither "dialogs" or +"profiles", since they're the first argument. The parameter name only exists +for documentation purposes. + +The term "chat" is used where a group or broadcast channel is expected. This +includes small groups, megagroups, broadcast channels and broadcast groups. +Telegram's API has, in the past, made a difference between which methods can +be used for "small group chats" and everything else. For example, small group +chats cannot have a public link (they automatically convert to megagroups). +Group permissions also used to be different, but because Telegram may unify +these eventually, the library attempts to hide this distinction. In general, +this is not something you should worry about. + + +Fetching profile information +============================ Through the use of the :ref:`sessions`, the library will automatically -remember the ID and hash pair, along with some extra information, so -you're able to just do this: +remember the packed users and chats, along with some extra information, +so you're able to just do this: .. code-block:: python @@ -106,145 +154,37 @@ you're able to just do this: # # Dialogs are the "conversations you have open". # This method returns a list of Dialog, which - # has the .entity attribute and other information. + # has the .user and .chat attributes (among others). # - # This part is IMPORTANT, because it fills the entity cache. + # This part is IMPORTANT, because it fills the cache. dialogs = await client.get_dialogs() - # All of these work and do the same. - username = await client.get_entity('username') - username = await client.get_entity('t.me/username') - username = await client.get_entity('https://telegram.dog/username') + # All of these work and do the same, but are more expensive to use. + channel = await client.get_profile('username') + channel = await client.get_profile('t.me/username') + channel = await client.get_profile('https://telegram.dog/username') + contact = await client.get_profile('+34xxxxxxxxx') - # Other kind of entities. - channel = await client.get_entity('telegram.me/joinchat/AAAAAEkk2WdoDrB4-Q8-gg') - contact = await client.get_entity('+34xxxxxxxxx') - friend = await client.get_entity(friend_id) + # This will work, but only if the ID is in cache. + friend = await client.get_profile(friend_id) - # Getting entities through their ID (User, Chat or Channel) - entity = await client.get_entity(some_id) - - # You can be more explicit about the type for said ID by wrapping - # it inside a Peer instance. This is recommended but not necessary. - from telethon.tl.types import PeerUser, PeerChat, PeerChannel - - my_user = await client.get_entity(PeerUser(some_id)) - my_chat = await client.get_entity(PeerChat(some_id)) - my_channel = await client.get_entity(PeerChannel(some_id)) + # This is the most reliable way to fetch a profile. + user = await client.get_profile('U.123.456789') + group = await client.get_profile('G.456.0') + broadcast = await client.get_profile('C.789.123456') -.. note:: - - You **don't** need to get the entity before using it! Just let the - library do its job. Use a phone from your contacts, username, ID or - input entity (preferred but not necessary), whatever you already have. - -All methods in the :ref:`telethon-client` call `.get_input_entity() -` prior -to sending the request to save you from the hassle of doing so manually. +All methods in the :ref:`telethon-client` accept any of the above +prior to sending the request to save you from the hassle of doing so manually. That way, convenience calls such as `client.send_message('username', 'hi!') -` -become possible. - -Every entity the library encounters (in any response to any call) will by -default be cached in the ``.session`` file (an SQLite database), to avoid -performing unnecessary API calls. If the entity cannot be found, additional -calls like :tl:`ResolveUsernameRequest` or :tl:`GetContactsRequest` may be -made to obtain the required information. - - -Entities vs. Input Entities -=========================== - -.. note:: - - This section is informative but worth reading. The library - will transparently handle all of these details for you. - -On top of the normal types, the API also make use of what they call their -``Input*`` versions of objects. The input version of an entity (e.g. -:tl:`InputPeerUser`, :tl:`InputChat`, etc.) only contains the minimum -information required from Telegram to identify -who you're referring to: a :tl:`Peer`'s **ID** and **hash**. They -are named like this because they are input parameters in the requests. - -Entities' IDs are the same for all user and bot accounts. However, the access -hash is **different for each account**, so trying to reuse the access hash -from one account in another will **not** work. - -Sometimes, Telegram only needs to indicate the entity type and their ID. For this purpose, :tl:`Peer` versions of the entities also -exist, which just have the ID. You cannot get the hash out of them since -you should not need it. The library probably has cached it before. - -Peers are enough to identify an entity, but they are not enough to make -a request with them. You need to know their hash before you can -"use them", and to know the hash you need to "encounter" them, let it -be in your dialogs, participants, message forwards, etc. - -.. note:: - - You *can* use peers with the library. Behind the scenes, they are - replaced with the input variant. Peers "aren't enough" on their own - , but the library will do some more work to use the right type. - -As we just mentioned, API calls don't need to know the whole information -about the entities, only their ID and hash. For this reason, another method, -`client.get_input_entity() ` -is available. This will always use the cache while possible, making zero API -calls most of the time. When a request is made, if you provided the full -entity, e.g. an :tl:`User`, the library will automatically convert it to the required -:tl:`InputPeer`. - -**You should always favour** -`client.get_input_entity() ` -**over** -`client.get_entity() ` -for this reason! Calling the latter will always make an API call to get -the most recent information about said entity, but invoking requests don't -need this information, just the :tl:`InputPeer`. Only use -`client.get_entity() ` -if you need to get actual information, like the username, name, title, etc. -of the entity. - -To further simplify the workflow, since the version ``0.16.2`` of the -library, the raw requests you make to the API are also able to call -`client.get_input_entity() ` -wherever needed, so you can even do things like: - -.. code-block:: python - - await client(SendMessageRequest('username', 'hello')) - -The library will call the request's ``.resolve()`` method, which will -resolve ``'username'`` with the appropriate :tl:`InputPeer`. Don't worry if -you don't get this yet, but remember that some of the details here are important. - - -Full Entities -============= - -In addition to :tl:`PeerUser`, :tl:`InputPeerUser`, :tl:`User` (and its -variants for chats and channels), there is also the concept of :tl:`UserFull`. - -This full variant has additional information such as whether the user is -blocked, its notification settings, the bio or about of the user, etc. - -There is also :tl:`messages.ChatFull` which is the equivalent of full entities -for chats and channels, with the about section of the channel. Note that -the ``users`` field only contains bots for the channel (so clients can -suggest commands to use). - -You can get both of these by invoking :tl:`GetFullUser`, :tl:`GetFullChat` -and :tl:`GetFullChannel` respectively. - - -Accessing Entities -================== +` become possible. +However, it can be expensive to fetch the username every time, so this is +better left for things which are not executed often. Although it's explicitly noted in the documentation that messages *subclass* `ChatGetter ` and `SenderGetter `, -some people still don't get inheritance. +this section will explain what this means. When the documentation says "Bases: `telethon.tl.custom.chatgetter.ChatGetter`" it means that the class you're looking at, *also* can act as the class it @@ -257,9 +197,9 @@ That means you can do this: .. code-block:: python - message.is_private message.chat_id - await message.get_chat() + message.chat + await event.get_chat() # ...etc `SenderGetter ` is similar: @@ -267,8 +207,8 @@ That means you can do this: .. code-block:: python message.user_id - await message.get_input_user() message.user + await event.get_input_user() # ...etc Quite a few things implement them, so it makes sense to reuse the code. @@ -277,11 +217,51 @@ For example, all events (except raw updates) implement `ChatGetter in some chat. +Packed User and packed Chat +=========================== + +A packed `User ` or a packed +`Chat ` can be thought of as +"a small string reference to the actual user or chat". + +It can easily be saved or embedded in the code for later use, +without having to worry if the user is in the session file cache. + +This "packed representation" is a compact way to store the type of the User +or Chat (is it a user account, a bot, a broadcast channel…), the identifier, +and the access hash. This "access hash" is something Telegram uses to ensure +that you can actually use this "User" or "Chat" in requests (so you can't just +create some random user identifier and expect it to work). + +In the raw API, this is pretty much "input peers", but the library uses the +term "packed user or chat" to refer to its custom type and string +representation. + +The User and Chat IDs are the same for all user and bot accounts. However, the +access hash is **different for each account**, so trying to reuse the access +hash from one account in another will **not** work. This also means the packed +representation will only work for the account that created it. + +The library needs to have this access hash in some way for it to work. +If it only has an ID and this ID is not in cache, it will not work. +If using the packed representation, the hash is embedded, and will always work. + +Every method, including raw API, will automatically convert your types to the +expected input type the API uses, meaning the following will work: + + +.. code-block:: python + + await client(_tl.fn.messages.SendMessage('username', 'hello')) + +(This is only a raw API example, there are better ways to send messages.) + + Summary ======= -TL;DR; If you're here because of *"Could not find the input entity for"*, -you must ask yourself, "how did I find this entity through official +TL;DR; If you're here because of *"Could not find the input peer for"*, +you must ask yourself, "how did I find this user or chat through official applications"? Now do the same with the library. Use what applies: .. code-block:: python @@ -289,7 +269,7 @@ applications"? Now do the same with the library. Use what applies: # (These examples assume you are inside an "async def") async with client: # Does it have a username? Use it! - entity = await client.get_entity(username) + user = await client.get_profile(username) # Do you have a conversation open with them? Get dialogs. await client.get_dialogs() @@ -297,16 +277,20 @@ applications"? Now do the same with the library. Use what applies: # Are they participants of some group? Get them. await client.get_participants('username') - # Is the entity the original sender of a forwarded message? Get it. + # Is the user the original sender of a forwarded message? Fetch the message. await client.get_messages('username', 100) # NOW you can use the ID anywhere! await client.send_message(123456, 'Hi!') - entity = await client.get_entity(123456) - print(entity) + user = await client.get_profile(123456) + print(user) -Once the library has "seen" the entity, you can use their **integer** ID. -You can't use entities from IDs the library hasn't seen. You must make the -library see them *at least once* and disconnect properly. You know where -the entities are, and you must tell the library. It won't guess for you. +Once the library has "seen" the user or chat, you can use their **integer** ID. +You can't use users or chats from IDs the library hasn't seen. You must make +the library see them *at least once* and disconnect properly. You know where +the user or chat are, and you must tell the library. It won't guess for you. + +This is why it's recommended to use the packed versions instead. They will +always work (unless Telegram, for some very unlikely reason, changes the way +using users and chats works, of course). diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 8bad852b..86ee2194 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -987,3 +987,4 @@ start also mandates phone= or password= as kwarg. qrlogin expires has been replaced with timeout and expired for parity with tos and auth. the goal is to hide the error-prone system clock and instead use asyncio's clock. recreate was removed (just call qr_login again; parity with get_tos). class renamed to QrLogin. now must be used in a contextmgr to prevent misuse. "entity" parameters have been renamed to "dialog" (user or chat expected) or "chat" (only chats expected), "profile" (if that makes sense). the goal is to move away from the entity terminology. this is intended to be a documentation change, but because the parameters were renamed, it's breaking. the expected usage of positional arguments is mostly unaffected. this includes the EntityLike hint. download_media param renamed message to media. iter_download file to media too +less types are supported to get entity (exact names, private links are undocumented but may work). get_entity is get_profile. get_input_entity is gone. get_peer_id is gone (if the isntance needs to be fetched anyway just use get_profile). diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 28e89531..a4958fe2 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -3279,57 +3279,49 @@ class TelegramClient: await client.sign_in(phone, code) """ - @forward_call(users.get_entity) - async def get_entity( + @forward_call(users.get_profile) + async def get_profile( self: 'TelegramClient', - entity: 'hints.EntitiesLike') -> 'hints.Entity': + profile: 'hints.DialogsLike') -> 'hints.Entity': """ - Turns the given entity into a valid Telegram :tl:`User`, :tl:`Chat` - or :tl:`Channel`. You can also pass a list or iterable of entities, - and they will be efficiently fetched from the network. + Turns the given profile reference into a `User ` + or `Chat ` instance. Arguments - entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`): + profile (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`): If a username is given, **the username will be resolved** making an API call every time. Resolving usernames is an expensive operation and will start hitting flood waits around 50 usernames in a short period of time. - If you want to get the entity for a *cached* username, you should - first `get_input_entity(username) ` which will - use the cache), and then use `get_entity` with the result of the - previous call. + Using phone numbers with strings will fetch your contact list first. - Similar limits apply to invite links, and you should use their - ID instead. + Using integer IDs will only work if the ID is in the session cache. - Using phone numbers (from people in your contact list), exact - names, integer IDs or :tl:`Peer` rely on a `get_input_entity` - first, which in turn needs the entity to be in cache, unless - a :tl:`InputPeer` was passed. + ``'me'`` is a special-case to the logged-in account (yourself). Unsupported types will raise ``TypeError``. - If the entity can't be found, ``ValueError`` will be raised. + If the user or chat can't be found, ``ValueError`` will be raised. Returns - :tl:`User`, :tl:`Chat` or :tl:`Channel` corresponding to the - input entity. A list will be returned if more than one was given. + `User ` or `Chat `, + depending on the profile requested. Example .. code-block:: python from telethon import utils - me = await client.get_entity('me') + me = await client.get_profile('me') print(utils.get_display_name(me)) - chat = await client.get_input_entity('username') + chat = await client.get_profile('username') async for message in client.get_messages(chat): ... # Note that you could have used the username directly, but it's - # good to use get_input_entity if you will reuse it a lot. + # good to use get_profile if you will reuse it a lot. async for message in client.get_messages('username'): ... diff --git a/telethon/_client/users.py b/telethon/_client/users.py index 154a778c..63138e53 100644 --- a/telethon/_client/users.py +++ b/telethon/_client/users.py @@ -150,9 +150,9 @@ async def is_user_authorized(self: 'TelegramClient') -> bool: except RpcError: return False -async def get_entity( +async def get_profile( self: 'TelegramClient', - entity: 'hints.EntitiesLike') -> 'hints.Entity': + profile: 'hints.DialogsLike') -> 'hints.Entity': single = not utils.is_list_like(entity) if single: entity = (entity,) @@ -222,7 +222,7 @@ async def get_entity( async def get_input_entity( self: 'TelegramClient', - peer: 'hints.EntityLike') -> '_tl.TypeInputPeer': + peer: 'hints.DialogLike') -> '_tl.TypeInputPeer': # Short-circuit if the input parameter directly maps to an InputPeer try: return utils.get_input_peer(peer) @@ -281,7 +281,7 @@ async def get_input_entity( pass raise ValueError( - 'Could not find the input entity for {} ({}). Please read https://' + 'Could not find the input peer for {} ({}). Please read https://' 'docs.telethon.dev/en/latest/concepts/entities.html to' ' find out more details.' .format(peer, type(peer).__name__) @@ -289,7 +289,7 @@ async def get_input_entity( async def get_peer_id( self: 'TelegramClient', - peer: 'hints.EntityLike') -> int: + peer: 'hints.DialogLike') -> int: if isinstance(peer, int): return utils.get_peer_id(peer) From 5ff8eb8159b7571a43d0f8e9fb2410fcf3399691 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 2 Mar 2022 12:40:42 +0100 Subject: [PATCH 244/256] Remove get_input_entity and get_peer_id --- telethon/_client/telegramclient.py | 79 ------------------------------ 1 file changed, 79 deletions(-) diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index a4958fe2..66673b73 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -3329,85 +3329,6 @@ class TelegramClient: some_id = await client.get_peer_id('+34123456789') """ - @forward_call(users.get_input_entity) - async def get_input_entity( - self: 'TelegramClient', - peer: 'hints.EntityLike') -> '_tl.TypeInputPeer': - """ - Turns the given entity into its input entity version. - - Most requests use this kind of :tl:`InputPeer`, so this is the most - suitable call to make for those cases. **Generally you should let the - library do its job** and don't worry about getting the input entity - first, but if you're going to use an entity often, consider making the - call: - - Arguments - entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`): - If a username or invite link is given, **the library will - use the cache**. This means that it's possible to be using - a username that *changed* or an old invite link (this only - happens if an invite link for a small group chat is used - after it was upgraded to a mega-group). - - If the username or ID from the invite link is not found in - the cache, it will be fetched. The same rules apply to phone - numbers (``'+34 123456789'``) from people in your contact list. - - If an exact name is given, it must be in the cache too. This - is not reliable as different people can share the same name - and which entity is returned is arbitrary, and should be used - only for quick tests. - - If a positive integer ID is given, the entity will be searched - in cached users, chats or channels, without making any call. - - If a negative integer ID is given, the entity will be searched - exactly as either a chat (prefixed with ``-``) or as a channel - (prefixed with ``-100``). - - If a :tl:`Peer` is given, it will be searched exactly in the - cache as either a user, chat or channel. - - If the given object can be turned into an input entity directly, - said operation will be done. - - Unsupported types will raise ``TypeError``. - - If the entity can't be found, ``ValueError`` will be raised. - - Returns - :tl:`InputPeerUser`, :tl:`InputPeerChat` or :tl:`InputPeerChannel` - or :tl:`InputPeerSelf` if the parameter is ``'me'`` or ``'self'``. - - Example - .. code-block:: python - - # If you're going to use "username" often in your code - # (make a lot of calls), consider getting its input entity - # once, and then using the "user" everywhere instead. - user = await client.get_input_entity('username') - - # The same applies to IDs, chats or channels. - chat = await client.get_input_entity(-123456789) - """ - - @forward_call(users.get_peer_id) - async def get_peer_id( - self: 'TelegramClient', - peer: 'hints.EntityLike') -> int: - """ - Gets the ID for the given entity. - - This method needs to be ``async`` because `peer` supports usernames, - invite-links, phone numbers (from people in your contact list), etc. - - Example - .. code-block:: python - - print(await client.get_peer_id('me')) - """ - # endregion Users # region Private From 6d293b440c347b2dbc8593479b705ffd88cf2b9c Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 2 Mar 2022 12:54:30 +0100 Subject: [PATCH 245/256] Update code using get_input_entity --- readthedocs/concepts/chats-vs-channels.rst | 4 +-- readthedocs/concepts/full-api.rst | 2 +- readthedocs/concepts/strings.rst | 2 +- readthedocs/examples/chats-and-channels.rst | 2 +- readthedocs/misc/v2-migration-guide.rst | 1 + .../quick-references/client-reference.rst | 4 +-- telethon/_client/bots.py | 4 +-- telethon/_client/chats.py | 36 +++++++++---------- telethon/_client/dialogs.py | 4 +-- telethon/_client/downloads.py | 6 ++-- telethon/_client/messageparse.py | 2 +- telethon/_client/messages.py | 26 +++++++------- telethon/_client/telegramclient.py | 10 +++++- telethon/_client/uploads.py | 2 +- telethon/_client/users.py | 18 +++++----- telethon/_events/chataction.py | 4 +-- telethon/types/_custom/draft.py | 2 +- telethon/types/_custom/inlineresult.py | 2 +- telethon/types/_custom/messagebutton.py | 4 +-- telethon_examples/gui.py | 2 +- telethon_generator/data/friendly.csv | 2 +- telethon_generator/generators/tlobject.py | 8 ++--- 22 files changed, 77 insertions(+), 70 deletions(-) diff --git a/readthedocs/concepts/chats-vs-channels.rst b/readthedocs/concepts/chats-vs-channels.rst index 87281373..51ffef7a 100644 --- a/readthedocs/concepts/chats-vs-channels.rst +++ b/readthedocs/concepts/chats-vs-channels.rst @@ -100,12 +100,12 @@ Note that this function can also work with other types, like :tl:`Chat` or :tl:`Channel` instances. If you need to convert other types like usernames which might need to perform -API calls to find out the identifier, you can use ``client.get_peer_id``: +API calls to find out the identifier, you can use ``client.get_profile``: .. code-block:: python - print(await client.get_peer_id('me')) # your id + print((await client.get_profile('me')).id) # your id If there is no "mark" (no minus sign), Telethon will assume your identifier diff --git a/readthedocs/concepts/full-api.rst b/readthedocs/concepts/full-api.rst index ac38e3d4..374633f4 100644 --- a/readthedocs/concepts/full-api.rst +++ b/readthedocs/concepts/full-api.rst @@ -129,7 +129,7 @@ as you wish. Remember to use the right types! To sum up: .. code-block:: python result = await client(SendMessageRequest( - await client.get_input_entity('username'), 'Hello there!' + await client.get_profile('username'), 'Hello there!' )) diff --git a/readthedocs/concepts/strings.rst b/readthedocs/concepts/strings.rst index a696b684..b8e25c12 100644 --- a/readthedocs/concepts/strings.rst +++ b/readthedocs/concepts/strings.rst @@ -8,7 +8,7 @@ does a result have? Well, the easiest thing to do is printing it: .. code-block:: python - entity = await client.get_entity('username') + entity = await client.get_profile('username') print(entity) That will show a huge **string** similar to the following: diff --git a/readthedocs/examples/chats-and-channels.rst b/readthedocs/examples/chats-and-channels.rst index 2c3823c3..4d84615b 100644 --- a/readthedocs/examples/chats-and-channels.rst +++ b/readthedocs/examples/chats-and-channels.rst @@ -107,7 +107,7 @@ use :tl:`GetMessagesViewsRequest`, setting ``increment=True``: .. code-block:: python - # Obtain `channel' through dialogs or through client.get_entity() or anyhow. + # Obtain `channel' through dialogs or through client.get_profile() or anyhow. # Obtain `msg_ids' through `.get_messages()` or anyhow. Must be a list. await client(GetMessagesViewsRequest( diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index 86ee2194..3c649b46 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -54,6 +54,7 @@ and identifier, rather than just the number. // TODO we DEFINITELY need to provide a way to "upgrade" old ids // TODO and storing type+number by hand is a pain, provide better alternative +// TODO get_peer_id is gone now too! Synchronous compatibility mode has been removed diff --git a/readthedocs/quick-references/client-reference.rst b/readthedocs/quick-references/client-reference.rst index a713907f..482ce320 100644 --- a/readthedocs/quick-references/client-reference.rst +++ b/readthedocs/quick-references/client-reference.rst @@ -118,9 +118,7 @@ Users get_me is_bot is_user_authorized - get_entity - get_input_entity - get_peer_id + get_profile Chats ----- diff --git a/telethon/_client/bots.py b/telethon/_client/bots.py index 22945cbf..e1e5fe5f 100644 --- a/telethon/_client/bots.py +++ b/telethon/_client/bots.py @@ -16,9 +16,9 @@ async def inline_query( dialog: 'hints.DialogLike' = None, offset: str = None, geo_point: '_tl.GeoPoint' = None) -> _custom.InlineResults: - bot = await self.get_input_entity(bot) + bot = await self._get_input_peer(bot) if dialog: - peer = await self.get_input_entity(dialog) + peer = await self._get_input_peer(dialog) else: peer = _tl.InputPeerEmpty() diff --git a/telethon/_client/chats.py b/telethon/_client/chats.py index 2bb34d45..f132d66d 100644 --- a/telethon/_client/chats.py +++ b/telethon/_client/chats.py @@ -30,7 +30,7 @@ class _ChatAction: return self._once().__await__() async def __aenter__(self): - self._request = dataclasses.replace(self._request, peer=await self._client.get_input_entity(self._request.peer)) + self._request = dataclasses.replace(self._request, peer=await self._client._get_input_peer(self._request.peer)) self._running = True self._task = asyncio.create_task(self._update()) return self @@ -47,7 +47,7 @@ class _ChatAction: self._task = None async def _once(self): - self._request = dataclasses.replace(self._request, peer=await self._client.get_input_entity(self._request.peer)) + self._request = dataclasses.replace(self._request, peer=await self._client._get_input_peer(self._request.peer)) await self._client(_tl.fn.messages.SetTyping(self._chat, self._action)) async def _update(self): @@ -114,7 +114,7 @@ class _ParticipantsIter(requestiter.RequestIter): else: raise RuntimeError('unhandled enum variant') - entity = await self.client.get_input_entity(entity) + entity = await self.client._get_input_peer(entity) ty = helpers._entity_type(entity) if search and (filter or ty != helpers._EntityType.CHANNEL): # We need to 'search' ourselves unless we have a PeerChannel @@ -172,7 +172,7 @@ class _ParticipantsIter(requestiter.RequestIter): else: self.total = 1 if self.limit != 0: - user = await self.client.get_entity(entity) + user = await self.client.get_profile(entity) if self.filter_entity(user): self.buffer.append(user) @@ -232,7 +232,7 @@ class _AdminLogIter(requestiter.RequestIter): else: events_filter = None - self.entity = await self.client.get_input_entity(entity) + self.entity = await self.client._get_input_peer(entity) admin_list = [] if admins: @@ -240,7 +240,7 @@ class _AdminLogIter(requestiter.RequestIter): admins = (admins,) for admin in admins: - admin_list.append(await self.client.get_input_entity(admin)) + admin_list.append(await self.client._get_input_peer(admin)) self.request = _tl.fn.channels.GetAdminLog( self.entity, q=search or '', min_id=min_id, max_id=max_id, @@ -278,7 +278,7 @@ class _ProfilePhotoIter(requestiter.RequestIter): async def _init( self, entity, offset, max_id ): - entity = await self.client.get_input_entity(entity) + entity = await self.client._get_input_peer(entity) ty = helpers._entity_type(entity) if ty == helpers._EntityType.USER: self.request = _tl.fn.photos.GetUserPhotos( @@ -481,8 +481,8 @@ async def edit_admin( anonymous: bool = None, is_admin: bool = None, title: str = None) -> _tl.Updates: - entity = await self.get_input_entity(chat) - user = await self.get_input_entity(user) + entity = await self._get_input_peer(chat) + user = await self._get_input_peer(user) ty = helpers._entity_type(user) perm_names = ( @@ -499,7 +499,7 @@ async def edit_admin( if post_messages or edit_messages: # TODO get rid of this once sessions cache this information if entity.channel_id not in self._megagroup_cache: - full_entity = await self.get_entity(entity) + full_entity = await self.get_profile(entity) self._megagroup_cache[entity.channel_id] = full_entity.megagroup if self._megagroup_cache[entity.channel_id]: @@ -545,7 +545,7 @@ async def edit_permissions( change_info: bool = True, invite_users: bool = True, pin_messages: bool = True) -> _tl.Updates: - entity = await self.get_input_entity(chat) + entity = await self._get_input_peer(chat) ty = helpers._entity_type(entity) rights = _tl.ChatBannedRights( @@ -570,7 +570,7 @@ async def edit_permissions( banned_rights=rights )) - user = await self.get_input_entity(user) + user = await self._get_input_peer(user) if isinstance(user, _tl.InputPeerSelf): raise ValueError('You cannot restrict yourself') @@ -586,8 +586,8 @@ async def kick_participant( chat: 'hints.DialogLike', user: 'typing.Optional[hints.DialogLike]' ): - entity = await self.get_input_entity(chat) - user = await self.get_input_entity(user) + entity = await self._get_input_peer(chat) + user = await self._get_input_peer(user) ty = helpers._entity_type(entity) if ty == helpers._EntityType.CHAT: @@ -620,14 +620,14 @@ async def get_permissions( chat: 'hints.DialogLike', user: 'hints.DialogLike' = None ) -> 'typing.Optional[_custom.ParticipantPermissions]': - entity = await self.get_entity(chat) + entity = await self.get_profile(chat) if not user: if helpers._entity_type(entity) != helpers._EntityType.USER: return entity.default_banned_rights - entity = await self.get_input_entity(entity) - user = await self.get_input_entity(user) + entity = await self._get_input_peer(entity) + user = await self._get_input_peer(user) if helpers._entity_type(entity) == helpers._EntityType.CHANNEL: participant = await self(_tl.fn.channels.GetParticipant( @@ -653,7 +653,7 @@ async def get_stats( chat: 'hints.DialogLike', message: 'typing.Union[int, _tl.Message]' = None, ): - entity = await self.get_input_entity(chat) + entity = await self._get_input_peer(chat) message = utils.get_message_id(message) if message is not None: diff --git a/telethon/_client/dialogs.py b/telethon/_client/dialogs.py index c1d12028..7b3b722f 100644 --- a/telethon/_client/dialogs.py +++ b/telethon/_client/dialogs.py @@ -122,7 +122,7 @@ class _DraftsIter(requestiter.RequestIter): peers = [] for entity in entities: peers.append(_tl.InputDialogPeer( - await self.client.get_input_entity(entity))) + await self.client._get_input_peer(entity))) r = await self.client(_tl.fn.messages.GetPeerDialogs(peers)) items = r.dialogs @@ -189,7 +189,7 @@ async def delete_dialog( else: deactivated = False - entity = await self.get_input_entity(dialog) + entity = await self._get_input_peer(dialog) ty = helpers._entity_type(entity) if ty == helpers._EntityType.CHANNEL: return await self(_tl.fn.channels.LeaveChannel(entity)) diff --git a/telethon/_client/downloads.py b/telethon/_client/downloads.py index f73b1412..0f919510 100644 --- a/telethon/_client/downloads.py +++ b/telethon/_client/downloads.py @@ -190,7 +190,7 @@ async def download_profile_photo( INPUTS = (0xc91c90b6, 0xe669bf46, 0x40f202fd) entity = profile if not isinstance(entity, tlobject.TLObject) or entity.SUBCLASS_OF_ID in INPUTS: - entity = await self.get_entity(entity) + entity = await self.get_profile(entity) possible_names = [] if entity.SUBCLASS_OF_ID not in ENTITIES: @@ -217,7 +217,7 @@ async def download_profile_photo( dc_id = photo.dc_id loc = _tl.InputPeerPhotoFileLocation( - peer=await self.get_input_entity(entity), + peer=await self._get_input_peer(entity), photo_id=photo.photo_id, big=thumb >= enums.Size.LARGE ) @@ -244,7 +244,7 @@ async def download_profile_photo( except errors.LocationInvalidError: # See issue #500, Android app fails as of v4.6.0 (1155). # The fix seems to be using the full channel chat photo. - ie = await self.get_input_entity(entity) + ie = await self._get_input_peer(entity) ty = helpers._entity_type(ie) if ty == helpers._EntityType.CHANNEL: full = await self(_tl.fn.channels.GetFullChannel(ie)) diff --git a/telethon/_client/messageparse.py b/telethon/_client/messageparse.py index ab828209..1111afe5 100644 --- a/telethon/_client/messageparse.py +++ b/telethon/_client/messageparse.py @@ -19,7 +19,7 @@ async def _replace_with_mention(self: 'TelegramClient', entities, i, user): try: entities[i] = _tl.InputMessageEntityMentionName( entities[i].offset, entities[i].length, - await self.get_input_entity(user) + await self._get_input_peer(user) ) return True except (ValueError, TypeError): diff --git a/telethon/_client/messages.py b/telethon/_client/messages.py index f5ab9ff3..c2f7ff89 100644 --- a/telethon/_client/messages.py +++ b/telethon/_client/messages.py @@ -28,7 +28,7 @@ class _MessagesIter(requestiter.RequestIter): ): # Note that entity being `None` will perform a global search. if entity: - self.entity = await self.client.get_input_entity(entity) + self.entity = await self.client._get_input_peer(entity) else: self.entity = None if self.reverse: @@ -62,8 +62,8 @@ class _MessagesIter(requestiter.RequestIter): offset_id = 1 if from_user: - from_user = await self.client.get_input_entity(from_user) - self.from_id = await self.client.get_peer_id(from_user) + from_user = await self.client._get_input_peer(from_user) + self.from_id = await self.client._get_peer_id(from_user) else: self.from_id = None @@ -272,7 +272,7 @@ class _IDsIter(requestiter.RequestIter): self.total = len(ids) self._ids = list(reversed(ids)) if self.reverse else ids self._offset = 0 - self._entity = (await self.client.get_input_entity(entity)) if entity else None + self._entity = (await self.client._get_input_peer(entity)) if entity else None self._ty = helpers._entity_type(self._entity) if self._entity else None # 30s flood wait every 300 messages (3 requests of 100 each, 30 of 10, etc.) @@ -327,7 +327,7 @@ async def _get_peer(self: 'TelegramClient', input_peer: 'hints.DialogLike'): return utils.get_peer(input_peer) except TypeError: # Can only be self by now - return _tl.PeerUser(await self.get_peer_id(input_peer)) + return _tl.PeerUser(await self._get_peer_id(input_peer)) def get_messages( @@ -466,7 +466,7 @@ async def send_message( elif not isinstance(message, InputMessage): raise TypeError(f'message must be either str, Message or InputMessage, but got: {message!r}') - entity = await self.get_input_entity(dialog) + entity = await self._get_input_peer(dialog) if comment_to is not None: entity, reply_to = await _get_comment_data(self, entity, comment_to) elif reply_to: @@ -540,11 +540,11 @@ async def forward_messages( if as_album is not None: warnings.warn('the as_album argument is deprecated and no longer has any effect') - entity = await self.get_input_entity(dialog) + entity = await self._get_input_peer(dialog) if from_dialog: - from_peer = await self.get_input_entity(from_dialog) - from_peer_id = await self.get_peer_id(from_peer) + from_peer = await self._get_input_peer(from_dialog) + from_peer_id = await self._get_peer_id(from_peer) else: from_peer = None from_peer_id = None @@ -632,7 +632,7 @@ async def edit_message( else: return await self(request) - entity = await self.get_input_entity(dialog) + entity = await self._get_input_peer(dialog) request = _tl.fn.messages.EditMessage( peer=entity, id=utils.get_message_id(message), @@ -659,7 +659,7 @@ async def delete_messages( ) if dialog: - entity = await self.get_input_entity(dialog) + entity = await self._get_input_peer(dialog) ty = helpers._entity_type(entity) else: # no entity (None), set a value that's not a channel for private delete @@ -689,7 +689,7 @@ async def mark_read( else: max_id = message.id - entity = await self.get_input_entity(dialog) + entity = await self._get_input_peer(dialog) if clear_mentions: await self(_tl.fn.messages.ReadMentions(entity)) @@ -726,7 +726,7 @@ async def unpin_message( async def _pin(self, entity, message, *, unpin, notify=False, pm_oneside=False): message = utils.get_message_id(message) or 0 - entity = await self.get_input_entity(entity) + entity = await self._get_input_peer(entity) if message <= 0: # old behaviour accepted negative IDs to unpin await self(_tl.fn.messages.UnpinAllMessages(entity)) return diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 66673b73..3c242647 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -3326,7 +3326,7 @@ class TelegramClient: ... # Note that for this to work the phone number must be in your contacts - some_id = await client.get_peer_id('+34123456789') + some_id = (await client.get_profile('+34123456789')).id """ # endregion Users @@ -3395,4 +3395,12 @@ class TelegramClient: progress_callback: 'hints.ProgressCallback' = None) -> '_tl.TypeInputFile': pass + @forward_call(users._get_input_peer) + async def _get_input_peer(self, *, save=True, **changes): + pass + + @forward_call(users._get_peer_id) + async def _get_peer_id(self, *, save=True, **changes): + pass + # endregion Private diff --git a/telethon/_client/uploads.py b/telethon/_client/uploads.py index c91acdcb..d5b79c81 100644 --- a/telethon/_client/uploads.py +++ b/telethon/_client/uploads.py @@ -168,7 +168,7 @@ async def _send_album(self: 'TelegramClient', entity, files, caption='', # In theory documents can be sent inside the albums but they appear # as different messages (not inside the album), and the logic to set # the attributes/avoid cache is already written in .send_file(). - entity = await self.get_input_entity(entity) + entity = await self._get_input_peer(entity) if not utils.is_list_like(caption): caption = (caption,) diff --git a/telethon/_client/users.py b/telethon/_client/users.py index 63138e53..909b15b4 100644 --- a/telethon/_client/users.py +++ b/telethon/_client/users.py @@ -166,7 +166,7 @@ async def get_profile( if isinstance(x, str): inputs.append(x) else: - inputs.append(await self.get_input_entity(x)) + inputs.append(await self._get_input_peer(x)) lists = { helpers._EntityType.USER: [], @@ -220,7 +220,7 @@ async def get_profile( return result[0] if single else result -async def get_input_entity( +async def _get_input_peer( self: 'TelegramClient', peer: 'hints.DialogLike') -> '_tl.TypeInputPeer': # Short-circuit if the input parameter directly maps to an InputPeer @@ -287,7 +287,7 @@ async def get_input_entity( .format(peer, type(peer).__name__) ) -async def get_peer_id( +async def _get_peer_id( self: 'TelegramClient', peer: 'hints.DialogLike') -> int: if isinstance(peer, int): @@ -296,9 +296,9 @@ async def get_peer_id( try: if peer.SUBCLASS_OF_ID not in (0x2d45687, 0xc91c90b6): # 0x2d45687, 0xc91c90b6 == crc32(b'Peer') and b'InputPeer' - peer = await self.get_input_entity(peer) + peer = await self._get_input_peer(peer) except AttributeError: - peer = await self.get_input_entity(peer) + peer = await self._get_input_peer(peer) if isinstance(peer, _tl.InputPeerSelf): peer = _tl.PeerUser(self._session_state.user_id) @@ -372,13 +372,13 @@ async def _get_input_dialog(self: 'TelegramClient', dialog): """ try: if dialog.SUBCLASS_OF_ID == 0xa21c9795: # crc32(b'InputDialogPeer') - return dataclasses.replace(dialog, peer=await self.get_input_entity(dialog.peer)) + return dataclasses.replace(dialog, peer=await self._get_input_peer(dialog.peer)) elif dialog.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer') return _tl.InputDialogPeer(dialog) except AttributeError: pass - return _tl.InputDialogPeer(await self.get_input_entity(dialog)) + return _tl.InputDialogPeer(await self._get_input_peer(dialog)) async def _get_input_notify(self: 'TelegramClient', notify): """ @@ -389,9 +389,9 @@ async def _get_input_notify(self: 'TelegramClient', notify): try: if notify.SUBCLASS_OF_ID == 0x58981615: if isinstance(notify, _tl.InputNotifyPeer): - return dataclasses.replace(notify, peer=await self.get_input_entity(notify.peer)) + return dataclasses.replace(notify, peer=await self._get_input_peer(notify.peer)) return notify except AttributeError: pass - return _tl.InputNotifyPeer(await self.get_input_entity(notify)) + return _tl.InputNotifyPeer(await self._get_input_peer(notify)) diff --git a/telethon/_events/chataction.py b/telethon/_events/chataction.py index 6b4d21e9..435ab459 100644 --- a/telethon/_events/chataction.py +++ b/telethon/_events/chataction.py @@ -307,7 +307,7 @@ class ChatAction(EventBuilder): Returns `added_by` but will make an API call if necessary. """ if not self.added_by and self._added_by: - self._added_by = await self._client.get_entity(self._added_by) + self._added_by = await self._client.get_profile(self._added_by) return self._added_by @@ -328,7 +328,7 @@ class ChatAction(EventBuilder): Returns `kicked_by` but will make an API call if necessary. """ if not self.kicked_by and self._kicked_by: - self._kicked_by = await self._client.get_entity(self._kicked_by) + self._kicked_by = await self._client.get_profile(self._kicked_by) return self._kicked_by diff --git a/telethon/types/_custom/draft.py b/telethon/types/_custom/draft.py index fba7f4d2..60aabdaf 100644 --- a/telethon/types/_custom/draft.py +++ b/telethon/types/_custom/draft.py @@ -58,7 +58,7 @@ class Draft: if not self.entity and await self.get_input_entity(): try: self._entity =\ - await self._client.get_entity(self._input_entity) + await self._client.get_profile(self._input_entity) except ValueError: pass diff --git a/telethon/types/_custom/inlineresult.py b/telethon/types/_custom/inlineresult.py index 84d8f64d..0a659f8e 100644 --- a/telethon/types/_custom/inlineresult.py +++ b/telethon/types/_custom/inlineresult.py @@ -143,7 +143,7 @@ class InlineResult: """ if entity: - entity = await self._client.get_input_entity(entity) + entity = await self._client.get_input_peer(entity) elif self._entity: entity = self._entity else: diff --git a/telethon/types/_custom/messagebutton.py b/telethon/types/_custom/messagebutton.py index ce5f7ed1..9286c042 100644 --- a/telethon/types/_custom/messagebutton.py +++ b/telethon/types/_custom/messagebutton.py @@ -72,7 +72,7 @@ class MessageButton: it will be "clicked" and the :tl:`BotCallbackAnswer` returned. If it's an inline :tl:`KeyboardButtonUserProfile` button, the - `client.get_entity` will be called and the resulting :tl:User will be + `client.get_profile` will be called and the resulting :tl:User will be returned. If it's an inline :tl:`KeyboardButtonSwitchInline` button, the @@ -112,7 +112,7 @@ class MessageButton: except BotResponseTimeoutError: return None elif isinstance(self.button, _tl.KeyboardButtonUserProfile): - return await self._client.get_entity(self.button.user_id) + return await self._client.get_profile(self.button.user_id) elif isinstance(self.button, _tl.KeyboardButtonSwitchInline): return await self._client(_tl.fn.messages.StartBot( bot=self._bot, peer=self._chat, start_param=self.button.query, diff --git a/telethon_examples/gui.py b/telethon_examples/gui.py index 78dcfb34..fb369c2f 100644 --- a/telethon_examples/gui.py +++ b/telethon_examples/gui.py @@ -322,7 +322,7 @@ class App(tkinter.Tk): try: old = self.chat_id # Valid chat ID, set it and configure the colour back to white - self.chat_id = await self.cl.get_peer_id(chat) + self.chat_id = (await self.cl.get_profile(chat)).id self.chat.configure(bg='white') # If the chat ID changed, clear the diff --git a/telethon_generator/data/friendly.csv b/telethon_generator/data/friendly.csv index 4d53c862..524746d5 100644 --- a/telethon_generator/data/friendly.csv +++ b/telethon_generator/data/friendly.csv @@ -23,4 +23,4 @@ messages.MessageMethods,delete_messages,channels.deleteMessages messages.deleteM messages.MessageMethods,send_read_acknowledge,messages.readMentions channels.readHistory messages.readHistory updates.UpdateMethods,catch_up,updates.getDifference updates.getChannelDifference uploads.UploadMethods,send_file,messages.sendMedia messages.sendMultiMedia messages.uploadMedia -users.UserMethods,get_entity,users.getUsers messages.getChats channels.getChannels contacts.resolveUsername +users.UserMethods,get_profile,users.getUsers messages.getChats channels.getChannels contacts.resolveUsername diff --git a/telethon_generator/generators/tlobject.py b/telethon_generator/generators/tlobject.py index 0893cca1..f52c497b 100644 --- a/telethon_generator/generators/tlobject.py +++ b/telethon_generator/generators/tlobject.py @@ -16,11 +16,11 @@ AUTO_GEN_NOTICE = \ AUTO_CASTS = { 'InputPeer': - 'utils.get_input_peer(await client.get_input_entity({}))', + 'utils.get_input_peer(await client._get_input_peer({}))', 'InputChannel': - 'utils.get_input_channel(await client.get_input_entity({}))', + 'utils.get_input_channel(await client._get_input_peer({}))', 'InputUser': - 'utils.get_input_user(await client.get_input_entity({}))', + 'utils.get_input_user(await client._get_input_peer({}))', 'InputDialogPeer': 'await client._get_input_dialog({})', 'InputNotifyPeer': 'await client._get_input_notify({})', @@ -33,7 +33,7 @@ AUTO_CASTS = { } NAMED_AUTO_CASTS = { - ('chat_id', 'int'): 'await client.get_peer_id({})' + ('chat_id', 'int'): 'await client._get_peer_id({})' } # Secret chats have a chat_id which may be negative. From 7413b6a6301c062f5596e3212f9a7e244ae77a27 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 2 Mar 2022 12:56:36 +0100 Subject: [PATCH 246/256] Fix typehint typo --- telethon/_misc/hints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/_misc/hints.py b/telethon/_misc/hints.py index b97cbc44..addce15c 100644 --- a/telethon/_misc/hints.py +++ b/telethon/_misc/hints.py @@ -20,7 +20,7 @@ DialogLike = typing.Union[ Dialog, FullDialog ] -DialogsLike = typing.Union[DialogLike, typing.Sequence[DialogsLike]] +DialogsLike = typing.Union[DialogLike, typing.Sequence[DialogLike]] ButtonLike = typing.Union[_tl.TypeKeyboardButton, _custom.Button] MarkupLike = typing.Union[ From 07aadf9fbfae1bf60f3276f14785c9fcbb458b53 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 2 Mar 2022 12:57:44 +0100 Subject: [PATCH 247/256] Fix get_profile name errors --- telethon/_client/users.py | 1 + 1 file changed, 1 insertion(+) diff --git a/telethon/_client/users.py b/telethon/_client/users.py index 909b15b4..42a83de2 100644 --- a/telethon/_client/users.py +++ b/telethon/_client/users.py @@ -153,6 +153,7 @@ async def is_user_authorized(self: 'TelegramClient') -> bool: async def get_profile( self: 'TelegramClient', profile: 'hints.DialogsLike') -> 'hints.Entity': + entity = profile single = not utils.is_list_like(entity) if single: entity = (entity,) From 2a750fa6964ffebecd7804953db3b413a1a83bc3 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 2 Mar 2022 13:01:28 +0100 Subject: [PATCH 248/256] Ignore ChannelParticipantLeft Closes #3252. --- telethon/_client/chats.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/telethon/_client/chats.py b/telethon/_client/chats.py index f132d66d..8737e576 100644 --- a/telethon/_client/chats.py +++ b/telethon/_client/chats.py @@ -205,6 +205,10 @@ class _ParticipantsIter(requestiter.RequestIter): else: user_id = participant.user_id + if isinstance(participant, types.ChannelParticipantLeft): + # These participants should be ignored. See #3231. + continue + user = users[user_id] if not self.filter_entity(user) or user.id in self.seen: continue From 4d6e75a54e12eb38a2a15d49386a00e6d70a05d7 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 2 Mar 2022 13:10:33 +0100 Subject: [PATCH 249/256] Fix update dispatch errors --- telethon/_client/updates.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index 5240e2f6..4ff05313 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -18,6 +18,7 @@ from .._events.base import StopPropagation, EventBuilder, EventHandler from .._events.filters import make_filter, NotResolved from .._misc import utils from .. import _tl +from ..types._custom import User, Chat if typing.TYPE_CHECKING: from .telegramclient import TelegramClient @@ -220,12 +221,13 @@ def _preprocess_updates(self, updates, users, chats): class Entities: def __init__(self, client, users, chats): self.self_id = client._session_state.user_id + self._client = client self._entities = {e.id: e for e in itertools.chain( - (User(client, u) for u in users), - (Chat(client, c) for u in chats), + (User._new(client, u) for u in users), + (Chat._new(client, c) for u in chats), )} - def get(self, client, peer): + def get(self, peer): if not peer: return None @@ -233,11 +235,11 @@ class Entities: try: return self._entities[id] except KeyError: - entity = client._entity_cache.get(query.user_id) + entity = self._client._entity_cache.get(query.user_id) if not entity: raise RuntimeError('Update is missing a hash but did not trigger a gap') - self._entities[entity.id] = User(client, entity) if entity.is_user else Chat(client, entity) + self._entities[entity.id] = User(self._client, entity) if entity.is_user else Chat(self._client, entity) return self._entities[entity.id] @@ -246,7 +248,7 @@ async def _dispatch(self, update, entities): try: event_cache = {} for handler in self._update_handlers: - event, entities = event_cache.get(handler._event) + event = event_cache.get(handler._event) if not event: # build can fail if we're missing an access hash; we want this to crash event_cache[handler._event] = event = handler._event._build(self, update, entities) From 0eb18f2f5a38e4a2717a6c0d6968f0ba04b96b72 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 2 Mar 2022 13:13:43 +0100 Subject: [PATCH 250/256] Raise TimeoutError from inline_query instead --- telethon/_client/bots.py | 20 ++++++++++++-------- telethon/_client/telegramclient.py | 4 ++++ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/telethon/_client/bots.py b/telethon/_client/bots.py index e1e5fe5f..b2b8c1c8 100644 --- a/telethon/_client/bots.py +++ b/telethon/_client/bots.py @@ -1,8 +1,9 @@ import typing +import asyncio from ..types import _custom from .._misc import hints -from .. import _tl +from .. import errors, _tl if typing.TYPE_CHECKING: from .telegramclient import TelegramClient @@ -22,12 +23,15 @@ async def inline_query( else: peer = _tl.InputPeerEmpty() - result = await self(_tl.fn.messages.GetInlineBotResults( - bot=bot, - peer=peer, - query=query, - offset=offset or '', - geo_point=geo_point - )) + try: + result = await self(_tl.fn.messages.GetInlineBotResults( + bot=bot, + peer=peer, + query=query, + offset=offset or '', + geo_point=geo_point + )) + except errors.BotResponseTimeoutError: + raise asyncio.TimeoutError from None return _custom.InlineResults(self, result, entity=peer if dialog else None) diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index 3c242647..3d32dd3b 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -733,6 +733,10 @@ class TelegramClient: The geo point location information to send to the bot for localised results. Available under some bots. + Raises + If the bot does not respond to the inline query in time, + `asyncio.TimeoutError` is raised. The timeout is decided by Telegram. + Returns A list of `_custom.InlineResult `. From 3ec0b132f035296f188e700f4adffddc972977b8 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 2 Mar 2022 13:15:00 +0100 Subject: [PATCH 251/256] Fix process_updates on updateShort not having users --- telethon/_updates/messagebox.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/telethon/_updates/messagebox.py b/telethon/_updates/messagebox.py index d25e0d09..87653103 100644 --- a/telethon/_updates/messagebox.py +++ b/telethon/_updates/messagebox.py @@ -318,8 +318,8 @@ class MessageBox: seq = getattr(updates, 'seq', None) or NO_SEQ seq_start = getattr(updates, 'seq_start', None) or seq - users = getattr(updates, 'users') or [] - chats = getattr(updates, 'chats') or [] + users = getattr(updates, 'users', None) or [] + chats = getattr(updates, 'chats', None) or [] updates = getattr(updates, 'updates', None) or [updates] # > For all the other [not `updates` or `updatesCombined`] `Updates` type constructors From fa6e5d95a9a3a2e786c3b0dcc91a3b5f483a8e28 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 2 Mar 2022 13:41:14 +0100 Subject: [PATCH 252/256] Fix parsing messages --- telethon/_client/messageparse.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/telethon/_client/messageparse.py b/telethon/_client/messageparse.py index 1111afe5..9e97a414 100644 --- a/telethon/_client/messageparse.py +++ b/telethon/_client/messageparse.py @@ -30,15 +30,12 @@ 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 = InputMessage._default_parse_mode + parse, _ = InputMessage._default_parse_mode else: - parse_mode = utils.sanitize_parse_mode(parse_mode) - - if not parse_mode: - return message, [] + parse, _ = utils.sanitize_parse_mode(parse_mode) original_message = message - message, msg_entities = parse_mode.parse(message) + message, msg_entities = parse(message) if original_message and not message and not msg_entities: raise ValueError("Failed to parse message") From 2c6497045161c009696314587c8646c41f31d930 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 2 Mar 2022 13:41:24 +0100 Subject: [PATCH 253/256] Fix remove_event_handler --- telethon/_client/updates.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/telethon/_client/updates.py b/telethon/_client/updates.py index 4ff05313..9882cf0a 100644 --- a/telethon/_client/updates.py +++ b/telethon/_client/updates.py @@ -148,10 +148,11 @@ def remove_event_handler( # slow-path, remove all matching removed = [] - for index, handler in reversed(enumerate(self._update_handlers)): - if callback is not None and handler.callback != callback: + for index in reversed(range(len(self._update_handlers))): + handler = self._update_handlers[index] + if callback is not None and handler._callback != callback: continue - if event is not None and handler.event != event: + if event is not None and handler._event != event: continue removed.append(self._update_handlers.pop(index)) From 08bb72ea6b49486efde98ba0100bbd32676646bf Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 2 Mar 2022 13:41:33 +0100 Subject: [PATCH 254/256] Fix inline builder articles --- telethon/types/_custom/inlinebuilder.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telethon/types/_custom/inlinebuilder.py b/telethon/types/_custom/inlinebuilder.py index 39b20a1c..9d69eb1f 100644 --- a/telethon/types/_custom/inlinebuilder.py +++ b/telethon/types/_custom/inlinebuilder.py @@ -1,4 +1,5 @@ import hashlib +import dataclasses from ... import _tl from ..._misc import utils @@ -144,7 +145,7 @@ class InlineBuilder: content=content ) if id is None: - result.id = hashlib.sha256(bytes(result)).hexdigest() + result = dataclasses.replace(result, id=hashlib.sha256(bytes(result)).hexdigest()) return result From 2357116f01eebb7e94803cbfb2ca384ecec97c32 Mon Sep 17 00:00:00 2001 From: James R T Date: Mon, 7 Mar 2022 00:47:35 +0800 Subject: [PATCH 255/256] Fix IPv6 typo to IPv4 for StringSession.save() (#3760) --- telethon/_sessions/string.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/_sessions/string.py b/telethon/_sessions/string.py index e63376af..eab1b0cc 100644 --- a/telethon/_sessions/string.py +++ b/telethon/_sessions/string.py @@ -77,7 +77,7 @@ class StringSession(MemorySession): if self.dcs[self.state.dc_id].ipv6 is not None: ip = self.dcs[self.state.dc_id].ipv6.to_bytes(16, 'big', signed=False) else: - ip = self.dcs[self.state.dc_id].ipv6.to_bytes(4, 'big', signed=False) + ip = self.dcs[self.state.dc_id].ipv4.to_bytes(4, 'big', signed=False) return CURRENT_VERSION + StringSession.encode(struct.pack( _STRUCT_PREFORMAT.format(len(ip)), From dd51aea4db90fd255a14e27192e221c70b45e105 Mon Sep 17 00:00:00 2001 From: luk0y <45989946+luk0y@users.noreply.github.com> Date: Wed, 18 May 2022 18:53:10 +0530 Subject: [PATCH 256/256] Fix broken url of Compatibility and Convenience (#3829) --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 15985350..40512dbd 100755 --- a/README.rst +++ b/README.rst @@ -79,7 +79,7 @@ useful information. .. _asyncio: https://docs.python.org/3/library/asyncio.html .. _MTProto: https://core.telegram.org/mtproto .. _Telegram: https://telegram.org -.. _Compatibility and Convenience: https://docs.telethon.dev/en/latest/misc/compatibility-and-convenience.html +.. _Compatibility and Convenience: https://docs.telethon.dev/en/stable/misc/compatibility-and-convenience.html .. _Read The Docs: https://docs.telethon.dev .. |logo| image:: logo.svg