From 7de1c0e237df5a5d9652030555ffcac33ca1f60b Mon Sep 17 00:00:00 2001 From: Andrew Lane <32808683+AndrewLaneX@users.noreply.github.com> Date: Tue, 13 Oct 2020 04:50:05 -0400 Subject: [PATCH 01/29] Document two new RPC errors (#1591) --- telethon_generator/data/errors.csv | 2 ++ telethon_generator/data/methods.csv | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/telethon_generator/data/errors.csv b/telethon_generator/data/errors.csv index ffd769f6..64168f4f 100644 --- a/telethon_generator/data/errors.csv +++ b/telethon_generator/data/errors.csv @@ -88,6 +88,7 @@ EMAIL_INVALID,400,The given email is invalid EMAIL_UNCONFIRMED_X,400,"Email unconfirmed, the length of the code must be {code_length}" 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 ENCRYPTED_MESSAGE_INVALID,400,Encrypted message invalid ENCRYPTION_ALREADY_ACCEPTED,400,Secret chat already accepted ENCRYPTION_ALREADY_DECLINED,400,The secret chat was already declined @@ -272,6 +273,7 @@ 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} 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 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 diff --git a/telethon_generator/data/methods.csv b/telethon_generator/data/methods.csv index d9a1ce0d..36830b47 100644 --- a/telethon_generator/data/methods.csv +++ b/telethon_generator/data/methods.csv @@ -114,7 +114,7 @@ channels.readHistory,user,CHANNEL_INVALID CHANNEL_PRIVATE channels.readMessageContents,user,CHANNEL_INVALID CHANNEL_PRIVATE channels.reportSpam,user,CHANNEL_INVALID INPUT_USER_DEACTIVATED channels.setDiscussionGroup,user,BROADCAST_ID_INVALID LINK_NOT_MODIFIED MEGAGROUP_ID_INVALID MEGAGROUP_PREHISTORY_HIDDEN -channels.setStickers,both,CHANNEL_INVALID PARTICIPANTS_TOO_FEW +channels.setStickers,both,CHANNEL_INVALID PARTICIPANTS_TOO_FEW STICKERSET_OWNER_ANONYMOUS channels.togglePreHistoryHidden,user,CHAT_LINK_EXISTS channels.toggleSignatures,user,CHANNEL_INVALID channels.toggleSlowMode,user,SECONDS_INVALID @@ -236,7 +236,7 @@ messages.getScheduledMessages,user, messages.getSearchCounters,user, messages.getSplitRanges,user, messages.getStatsURL,user, -messages.getStickerSet,both,STICKERSET_INVALID +messages.getStickerSet,both,EMOTICON_STICKERPACK_MISSING STICKERSET_INVALID messages.getStickers,user,EMOTICON_EMPTY messages.getSuggestedDialogFilters,user, messages.getUnreadMentions,user,PEER_ID_INVALID From 15f7c27bce847be357cf560234455f6e6b9c8e9a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 15 Oct 2020 09:27:37 +0200 Subject: [PATCH 02/29] Fix .photo()/.document() inline results excluding media from msg --- telethon/tl/custom/inlinebuilder.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/telethon/tl/custom/inlinebuilder.py b/telethon/tl/custom/inlinebuilder.py index 223ba0d6..ea9d4c29 100644 --- a/telethon/tl/custom/inlinebuilder.py +++ b/telethon/tl/custom/inlinebuilder.py @@ -144,6 +144,7 @@ class InlineBuilder: text=text or '', parse_mode=parse_mode, link_preview=link_preview, + media=True, geo=geo, period=period, contact=contact, @@ -225,6 +226,7 @@ class InlineBuilder: text=text or '', parse_mode=parse_mode, link_preview=link_preview, + media=True, geo=geo, period=period, contact=contact, @@ -270,7 +272,7 @@ class InlineBuilder: async def _message( self, *, - text=None, parse_mode=(), link_preview=True, + text=None, parse_mode=(), link_preview=True, media=False, geo=None, period=60, contact=None, game=False, buttons=None ): # Empty strings are valid but false-y; if they're empty use dummy '\0' @@ -284,18 +286,25 @@ class InlineBuilder: markup = self._client.build_reply_markup(buttons, inline_only=True) if text is not None: - if not text: # Automatic media on empty string, like stickers - return types.InputBotInlineMessageMediaAuto('') - text, msg_entities = await self._client._parse_message_text( text, parse_mode ) - return types.InputBotInlineMessageText( - message=text, - no_webpage=not link_preview, - entities=msg_entities, - reply_markup=markup - ) + if media: + # "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( + message=text, + entities=msg_entities, + reply_markup=markup + ) + else: + return types.InputBotInlineMessageText( + message=text, + no_webpage=not link_preview, + entities=msg_entities, + reply_markup=markup + ) elif isinstance(geo, (types.InputGeoPoint, types.GeoPoint)): return types.InputBotInlineMessageMediaGeo( geo_point=utils.get_input_geo(geo), From 7c3bbaca2a738934b2d4741739fd10837b0f0fbd Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 15 Oct 2020 10:40:19 +0200 Subject: [PATCH 03/29] Support not including the media from inline results in the msg --- telethon/tl/custom/inlinebuilder.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/telethon/tl/custom/inlinebuilder.py b/telethon/tl/custom/inlinebuilder.py index ea9d4c29..f0338cf9 100644 --- a/telethon/tl/custom/inlinebuilder.py +++ b/telethon/tl/custom/inlinebuilder.py @@ -110,7 +110,7 @@ class InlineBuilder: # noinspection PyIncorrectDocstring async def photo( - self, file, *, id=None, + self, file, *, id=None, include_media=True, text=None, parse_mode=(), link_preview=True, geo=None, period=60, contact=None, game=False, buttons=None ): @@ -118,6 +118,11 @@ class InlineBuilder: Creates a new inline result of photo type. Args: + include_media (`bool`, optional): + Whether the photo file used to display the result should be + included in the message itself or not. By default, the photo + is included, and the text parameter alters the caption. + file (`obj`, optional): Same as ``file`` for `client.send_file() `. @@ -144,7 +149,7 @@ class InlineBuilder: text=text or '', parse_mode=parse_mode, link_preview=link_preview, - media=True, + media=include_media, geo=geo, period=period, contact=contact, @@ -163,7 +168,8 @@ class InlineBuilder: 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, - geo=None, period=60, contact=None, game=False, buttons=None + geo=None, period=60, contact=None, game=False, buttons=None, + include_media=True ): """ Creates a new inline result of document type. @@ -188,6 +194,11 @@ class InlineBuilder: mpeg4_gif, video, audio, voice, document, sticker. See "Type of the result" in https://core.telegram.org/bots/api. + + include_media (`bool`, optional): + Whether the document file used to display the result should be + included in the message itself or not. By default, the document + is included, and the text parameter alters the caption. """ if type is None: if voice_note: @@ -226,7 +237,7 @@ class InlineBuilder: text=text or '', parse_mode=parse_mode, link_preview=link_preview, - media=True, + media=include_media, geo=geo, period=period, contact=contact, From 9c5b9abb937069b8b327b2a8b4dbb757a635043a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 15 Oct 2020 10:40:41 +0200 Subject: [PATCH 04/29] Fix sending of documents in inline results --- telethon/tl/custom/inlinebuilder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telethon/tl/custom/inlinebuilder.py b/telethon/tl/custom/inlinebuilder.py index f0338cf9..38c50311 100644 --- a/telethon/tl/custom/inlinebuilder.py +++ b/telethon/tl/custom/inlinebuilder.py @@ -204,7 +204,7 @@ class InlineBuilder: if voice_note: type = 'voice' else: - type = 'document' + type = 'file' try: fh = utils.get_input_document(file) From 312dac90a3df6dde0e7ccd038124c554d8f33915 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 15 Oct 2020 10:42:40 +0200 Subject: [PATCH 05/29] Improve inline result documentation with more examples --- telethon/events/inlinequery.py | 3 ++ telethon/tl/custom/inlinebuilder.py | 75 +++++++++++++++++++++++++++-- telethon/tl/custom/inlineresult.py | 4 ++ 3 files changed, 78 insertions(+), 4 deletions(-) diff --git a/telethon/events/inlinequery.py b/telethon/events/inlinequery.py index 7a789669..ad3dbcf6 100644 --- a/telethon/events/inlinequery.py +++ b/telethon/events/inlinequery.py @@ -147,6 +147,9 @@ class InlineQuery(EventBuilder): """ 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. diff --git a/telethon/tl/custom/inlinebuilder.py b/telethon/tl/custom/inlinebuilder.py index 38c50311..a92f6c7b 100644 --- a/telethon/tl/custom/inlinebuilder.py +++ b/telethon/tl/custom/inlinebuilder.py @@ -83,6 +83,31 @@ class InlineBuilder: content (:tl:`InputWebDocument`, optional): The content to be shown for this result. For now it has to be a :tl:`InputWebDocument` if present. + + Example: + .. code-block:: python + + results = [ + # Option with title and description sending a message. + builder.article( + title='First option', + description='This is the first option', + text='Text sent after clicking this option', + ), + # Option with title URL to be opened when clicked. + builder.article( + title='Second option', + url='https://example.com', + text='Text sent if the user clicks the option and not the URL', + ), + # Sending a message with buttons. + # You can use a list or a list of lists to include more buttons. + builder.article( + title='Third option', + text='Text sent with buttons below', + buttons=Button.url('https://example.com'), + ), + ] """ # TODO Does 'article' work always? # article, photo, gif, mpeg4_gif, video, audio, @@ -126,6 +151,28 @@ class InlineBuilder: file (`obj`, optional): Same as ``file`` for `client.send_file() `. + + Example: + .. code-block:: python + + results = [ + # Sending just the photo when the user selects it. + builder.photo('/path/to/photo.jpg'), + + # Including a caption with some in-memory photo. + photo_bytesio = ... + builder.photo( + photo_bytesio, + text='This will be the caption of the sent photo', + ), + + # Sending just the message without including the photo. + builder.photo( + photo, + text='This will be a normal text message', + include_media=False, + ), + ] """ try: fh = utils.get_input_photo(file) @@ -190,15 +237,35 @@ class InlineBuilder: Further explanation of what this result means. type (`str`, optional): - The type of the document. May be one of: photo, gif, - mpeg4_gif, video, audio, voice, document, sticker. - - See "Type of the result" in https://core.telegram.org/bots/api. + The type of the document. May be one of: article, audio, + contact, file, geo, gif, photo, sticker, venue, video, voice. include_media (`bool`, optional): Whether the document file used to display the result should be included in the message itself or not. By default, the document is included, and the text parameter alters the caption. + + Example: + .. code-block:: python + + results = [ + # Sending just the file when the user selects it. + builder.document('/path/to/file.pdf'), + + # Including a caption with some in-memory file. + file_bytesio = ... + builder.document( + file_bytesio, + text='This will be the caption of the sent file', + ), + + # Sending just the message without including the file. + builder.document( + photo, + text='This will be a normal text message', + include_media=False, + ), + ] """ if type is None: if voice_note: diff --git a/telethon/tl/custom/inlineresult.py b/telethon/tl/custom/inlineresult.py index 9d08d600..f189068c 100644 --- a/telethon/tl/custom/inlineresult.py +++ b/telethon/tl/custom/inlineresult.py @@ -13,6 +13,10 @@ class InlineResult: result (:tl:`BotInlineResult`): The original :tl:`BotInlineResult` object. """ + # tdlib types are the following (InlineQueriesManager::answer_inline_query @ 1a4a834): + # gif, article, audio, contact, file, geo, photo, sticker, venue, video, voice + # + # However, those documented in https://core.telegram.org/bots/api#inline-mode are different. ARTICLE = 'article' PHOTO = 'photo' GIF = 'gif' From 3ff09f7b91aa8fdd097169ec1544ab0b59483a1f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 15 Oct 2020 11:04:54 +0200 Subject: [PATCH 06/29] Use inline result mime to infer the result type --- telethon/tl/custom/inlinebuilder.py | 25 ++++++++++++++++++++++++- telethon_generator/data/errors.csv | 1 + telethon_generator/data/methods.csv | 2 +- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/telethon/tl/custom/inlinebuilder.py b/telethon/tl/custom/inlinebuilder.py index a92f6c7b..949f0b63 100644 --- a/telethon/tl/custom/inlinebuilder.py +++ b/telethon/tl/custom/inlinebuilder.py @@ -3,6 +3,20 @@ import hashlib from .. import functions, types from ... import utils +_TYPE_TO_MIMES = { + 'gif': ['image/gif'], # 'video/mp4' too, but that's used for video + 'article': ['text/html'], + 'audio': ['audio/mpeg'], + 'contact': [], + 'file': ['application/pdf', 'application/zip'], # actually any + 'geo': [], + 'photo': ['image/jpeg'], + 'sticker': ['image/webp', 'application/x-tgsticker'], + 'venue': [], + 'video': ['video/mp4'], # tdlib includes text/html for some reason + 'voice': ['audio/ogg'], +} + class InlineBuilder: """ @@ -239,6 +253,8 @@ class InlineBuilder: type (`str`, optional): The type of the document. May be one of: article, audio, contact, file, geo, gif, photo, sticker, venue, video, voice. + It will be automatically set if ``mime_type`` is specified, + and default to ``'file'`` if no matching mime type is found. include_media (`bool`, optional): Whether the document file used to display the result should be @@ -270,7 +286,14 @@ class InlineBuilder: if type is None: if voice_note: type = 'voice' - else: + elif mime_type: + for ty, mimes in _TYPE_TO_MIMES.items(): + for mime in mimes: + if mime_type == mime: + type = ty + break + + if type is None: type = 'file' try: diff --git a/telethon_generator/data/errors.csv b/telethon_generator/data/errors.csv index 64168f4f..1ca79418 100644 --- a/telethon_generator/data/errors.csv +++ b/telethon_generator/data/errors.csv @@ -9,6 +9,7 @@ ADMIN_RANK_INVALID,400,The given admin title or rank was invalid (possibly large API_ID_INVALID,400,The api_id/api_hash combination is invalid API_ID_PUBLISHED_FLOOD,400,"This API id was published somewhere, you can't use it now" ARTICLE_TITLE_EMPTY,400,The title of the article is empty +AUDIO_TITLE_EMPTY,400,The title attribute of the audio must be non-empty AUTH_BYTES_INVALID,400,The provided authorization is invalid AUTH_KEY_DUPLICATED,406,"The authorization key (session file) was used under two different IP addresses simultaneously, and can no longer be used. Use the same session exclusively, or use different sessions" AUTH_KEY_INVALID,401,The key is invalid diff --git a/telethon_generator/data/methods.csv b/telethon_generator/data/methods.csv index 36830b47..5e94a49f 100644 --- a/telethon_generator/data/methods.csv +++ b/telethon_generator/data/methods.csv @@ -282,7 +282,7 @@ messages.setBotPrecheckoutResults,both,ERROR_TEXT_EMPTY messages.setBotShippingResults,both,QUERY_ID_INVALID messages.setEncryptedTyping,user,CHAT_ID_INVALID messages.setGameScore,bot,PEER_ID_INVALID USER_BOT_REQUIRED -messages.setInlineBotResults,bot,ARTICLE_TITLE_EMPTY BUTTON_DATA_INVALID BUTTON_TYPE_INVALID BUTTON_URL_INVALID MESSAGE_EMPTY 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 WEBDOCUMENT_URL_INVALID +messages.setInlineBotResults,bot,ARTICLE_TITLE_EMPTY AUDIO_TITLE_EMPTY BUTTON_DATA_INVALID BUTTON_TYPE_INVALID BUTTON_URL_INVALID MESSAGE_EMPTY 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 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 From 4e1f582b17370e72a22a5b4de1c4d72ed068b06b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 15 Oct 2020 11:43:35 +0200 Subject: [PATCH 07/29] Call sign_in during sign_up if needed to send the code --- telethon/client/auth.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/telethon/client/auth.py b/telethon/client/auth.py index 8c91a694..8e9cb0ef 100644 --- a/telethon/client/auth.py +++ b/telethon/client/auth.py @@ -427,6 +427,23 @@ class AuthMethods: 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) From 5952a40c6dc43675011691e7fcc60156445922ba Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 16 Oct 2020 10:39:02 +0200 Subject: [PATCH 08/29] Update iter_messages to support fetching channel comments Closes #1598. --- telethon/client/messages.py | 46 +++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/telethon/client/messages.py b/telethon/client/messages.py index 6ec6ad24..7574142f 100644 --- a/telethon/client/messages.py +++ b/telethon/client/messages.py @@ -19,7 +19,7 @@ class _MessagesIter(RequestIter): """ async def _init( self, entity, offset_id, min_id, max_id, - from_user, offset_date, add_offset, filter, search + from_user, offset_date, add_offset, filter, search, reply_to ): # Note that entity being `None` will perform a global search. if entity: @@ -87,6 +87,18 @@ class _MessagesIter(RequestIter): offset_id=offset_id, limit=1 ) + elif reply_to is not None: + self.request = functions.messages.GetRepliesRequest( + peer=self.entity, + msg_id=reply_to, + offset_id=offset_id, + offset_date=offset_date, + add_offset=add_offset, + limit=1, + max_id=0, + min_id=0, + hash=0 + ) elif search is not None or filter or from_user: # Telegram completely ignores `from_id` in private chats ty = helpers._entity_type(self.entity) @@ -236,7 +248,7 @@ class _MessagesIter(RequestIter): # (only for the first request), it's safe to just clear it off. self.request.max_date = None else: - # getHistory and searchGlobal call it offset_date + # getHistory, searchGlobal and getReplies call it offset_date self.request.offset_date = last_message.date if isinstance(self.request, functions.messages.SearchGlobalRequest): @@ -325,7 +337,8 @@ class MessageMethods: from_user: 'hints.EntityLike' = None, wait_time: float = None, ids: 'typing.Union[int, typing.Sequence[int]]' = None, - reverse: bool = False + reverse: bool = False, + reply_to: int = None ) -> 'typing.Union[_MessagesIter, _IDsIter]': """ Iterator over the messages for the given chat. @@ -433,6 +446,26 @@ class MessageMethods: 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. + Yields Instances of `Message `. @@ -459,6 +492,10 @@ class MessageMethods: 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): @@ -486,7 +523,8 @@ class MessageMethods: offset_date=offset_date, add_offset=add_offset, filter=filter, - search=search + search=search, + reply_to=reply_to ) async def get_messages(self: 'TelegramClient', *args, **kwargs) -> 'hints.TotalList': From 1311b9393c2ac80c35761f7891373e874c57ad17 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 16 Oct 2020 11:00:14 +0200 Subject: [PATCH 09/29] Move alternative libraries to the wiki It doesn't make sense to track what happens to Telegram's ecosystem in the repository of a specific library. The wiki is better suited for this and can be trivially updated by anyone, allowing it to better evolve. --- .../telegram-api-in-other-languages.rst | 85 ++----------------- 1 file changed, 8 insertions(+), 77 deletions(-) diff --git a/readthedocs/developing/telegram-api-in-other-languages.rst b/readthedocs/developing/telegram-api-in-other-languages.rst index 943a4a1c..4e54126e 100644 --- a/readthedocs/developing/telegram-api-in-other-languages.rst +++ b/readthedocs/developing/telegram-api-in-other-languages.rst @@ -2,81 +2,12 @@ Telegram API in Other Languages =============================== +Telethon was made for **Python**, and it has inspired other libraries such as +`gramjs `__ (JavaScript) and `grammers +`__ (Rust). But there is a lot more beyond +those, made independently by different developers. -Telethon was made for **Python**, and as far as I know, there is no -*exact* port to other languages. However, there *are* other -implementations made by awesome people (one needs to be awesome to -understand the official Telegram documentation) on several languages -(even more Python too), listed below: - -C -= - -Possibly the most well-known unofficial open source implementation out -there by `@vysheng `__, -`tgl `__, and its console client -`telegram-cli `__. Latest development -has been moved to `BitBucket `__. - -C++ -=== - -The newest (and official) library, written from scratch, is called -`tdlib `__ and is what the Telegram X -uses. You can find more information in the official documentation, -published `here `__. - -JavaScript -========== - -`Ali Gasymov `__ made the `@mtproto/core `__ library for the browser and nodejs installable via `npm `__. - -`painor `__ is the primary author of `gramjs `__, -a Telegram client implementation in JavaScript. - -Kotlin -====== - -`Kotlogram `__ is a Telegram -implementation written in Kotlin (one of the -`official `__ -languages for -`Android `__) by -`@badoualy `__, currently as a beta– -yet working. - -Language-Agnostic -================= - -`Taas `__ is a service that lets you use Telegram API with any HTTP client via API. Using tdlib under the hood, Taas is commercial service, but allows free access if you use under 5000 requests per month. - -PHP -=== - -A PHP implementation is also available thanks to -`@danog `__ and his -`MadelineProto `__ project, with -a very nice `online -documentation `__ too. - -Python -====== - -A fairly new (as of the end of 2017) Telegram library written from the -ground up in Python by -`@delivrance `__ and his -`Pyrogram `__ library. -There isn't really a reason to pick it over Telethon and it'd be kinda -sad to see you go, but it would be nice to know what you miss from each -other library in either one so both can improve. - -Rust -==== - -The `grammers `__ library is made by -the `same author as Telethon's `__! If you are -looking for a Telethon alternative written in Rust, this is a valid option! - -Another older, work-in-progress implementation, on Rust is made by -`@JuanPotato `__ under the fancy -name of `Vail `__. +If you're looking for something like Telethon but in a different programming +language, head over to `Telegram API in Other Languages in the official wiki +`__ +for a (mostly) up-to-date list. From 94ce3b06eb0f5ba3e5218065a4c329d3a2a05506 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 18 Oct 2020 13:10:37 +0200 Subject: [PATCH 10/29] Add missing raw API fields to Message and re-order them Keeping them in order is important to easily change them when new things are added so that we don't miss them again on another update. --- telethon/tl/custom/message.py | 99 ++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 48 deletions(-) diff --git a/telethon/tl/custom/message.py b/telethon/tl/custom/message.py index 4e182ca5..e0b2d24c 100644 --- a/telethon/tl/custom/message.py +++ b/telethon/tl/custom/message.py @@ -21,10 +21,6 @@ class Message(ChatGetter, SenderGetter, TLObject, abc.ABC): have access to all their sender and chat properties and methods. Members: - id (`int`): - The ID of this message. This field is *always* present. - Any other member is optional and may be `None`. - out (`bool`): Whether the message is outgoing (i.e. you sent it from another session) or incoming (i.e. someone else sent it). @@ -44,7 +40,6 @@ class Message(ChatGetter, SenderGetter, TLObject, abc.ABC): 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 @@ -62,11 +57,35 @@ class Message(ChatGetter, SenderGetter, TLObject, abc.ABC): 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. + + 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 @@ -77,26 +96,6 @@ class Message(ChatGetter, SenderGetter, TLObject, abc.ABC): ` instances, which will be `None` for other types of messages. - action (:tl:`MessageAction`): - The message action object of the message for :tl:`MessageService` - instances, which will be `None` for other types of messages. - - 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. - - reply_to (:tl:`MessageReplyHeader`): - The original reply header if this message is replying to another. - - 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"). - media (:tl:`MessageMedia`): The media sent with this message if any (such as photos, videos, documents, gifs, stickers, etc.). @@ -120,13 +119,15 @@ class Message(ChatGetter, SenderGetter, TLObject, abc.ABC): 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. - edit_hide (`bool`): - Whether the edited mark of this message is edited - should be hidden (e.g. in GUI clients) or shown. - post_author (`str`): The display name of the message sender to show in messages sent to broadcast channels. @@ -139,6 +140,10 @@ class Message(ChatGetter, SenderGetter, TLObject, abc.ABC): 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. + + action (:tl:`MessageAction`): + The message action object of the message for :tl:`MessageService` + instances, which will be `None` for other types of messages. """ # region Initialization @@ -166,35 +171,33 @@ class Message(ChatGetter, SenderGetter, TLObject, abc.ABC): # For MessageAction (mandatory) action=None): - # Common properties to all messages - self.id = id - self.peer_id = peer_id - self.date = date + # Common properties to messages, then to service (in the order they're defined in the `.tl`) self.out = out self.mentioned = mentioned self.media_unread = media_unread self.silent = silent self.post = post - self.from_id = from_id - self.reply_to = reply_to - self.message = message - self.fwd_from = fwd_from - self.via_bot_id = via_bot_id - self.media = None if isinstance( - media, types.MessageMediaEmpty) else media - - self.reply_markup = reply_markup - self.entities = entities - self.views = views - self.edit_date = edit_date - self.post_author = post_author - self.grouped_id = grouped_id self.from_scheduled = from_scheduled self.legacy = legacy self.edit_hide = edit_hide - self.restriction_reason = restriction_reason + 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, types.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.post_author = post_author + self.grouped_id = grouped_id + self.restriction_reason = restriction_reason self.action = action # Convenient storage for custom functions From 4db51dff8a419d01e7c59c01403b2fdd03b9066a Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 18 Oct 2020 14:11:52 +0200 Subject: [PATCH 11/29] Update to v1.17 --- readthedocs/misc/changelog.rst | 130 +++++++++++++++++++++++++++++++++ telethon/client/chats.py | 5 ++ telethon/version.py | 2 +- 3 files changed, 136 insertions(+), 1 deletion(-) diff --git a/readthedocs/misc/changelog.rst b/readthedocs/misc/changelog.rst index 8d6484de..9f8cacfa 100644 --- a/readthedocs/misc/changelog.rst +++ b/readthedocs/misc/changelog.rst @@ -13,6 +13,136 @@ it can take advantage of new goodies! .. contents:: List of All Versions +Channel comments and Anonymous Admins (v1.17) +============================================= + ++------------------------+ +| Scheme layer used: 119 | ++------------------------+ + +New minor version, new layer change! This time is a good one to remind every +consumer of Python libraries that **you should always specify fixed versions +of your dependencies**! If you're using a ``requirements.txt`` file and you +want to stick with the old version (or any version) for the time being, you +can `use the following syntax `__: + +.. code-block:: text + + telethon~=1.16.0 + +This will install any version compatible with the written version (so, any in +the ``1.16`` series). Patch releases will never break your code (and if they +do, it's a bug). You can also use that syntax in ``pip install``. Your code +can't know what new versions will look like, so saying it will work with all +versions is a lie and will cause issues. + +The reason to bring this up is that Telegram has changed things again, and +with the introduction of anonymous administrators and channel comments, the +sender of a message may not be a :tl:`User`! To accomodate for this, the field +is now a :tl:`Peer` and not `int`. As a reminder, it's always a good idea to +use Telethon's friendly methods and custom properties, which have a higher +stability guarantee than accessing raw API fields. + +Even if you don't update, your code will still need to account for the fact +that the sender of a message might be one of the accounts Telegram introduced +to preserve backwards compatibility, because this is a server-side change, so +it's better to update and not lag behind. As it's mostly just a single person +driving the project on their free time, bug-fixes are not backported. + +This version also updates the format of SQLite sessions (the default), so +after upgrading and using an old session, the session will be updated, which +means trying to use it back in older versions of the library won't work. + +For backwards-compatibility sake, the library has introduced the properties +`Message.reply_to_msg_id ` +and `Message.to_id ` that behave +like they did before (Telegram has renamed and changed how these fields work). + + +Breaking Changes +~~~~~~~~~~~~~~~~ + +* ``Message.from_id`` is now a :tl:`Peer`, not `int`! If you want the marked + sender ID (much like old behaviour), replace all uses of ``.from_id`` with + ``.sender_id``. This will mostly work, but of course in old and new versions + you have to account for the fact that this sender may no longer be a user. +* You can no longer assign to `Message.reply_to_msg_id + ` and `Message.to_id + ` because these are now properties + that offer a "view" to the real value from a different field. +* Answering inline queries with a ``photo`` or ``document`` will now send the + photo or document used in the resulting message by default. Not sending the + media was technically a bug, but some people may be relying on this old + behaviour. You can use the old behaviour with ``include_media=False``. + +Additions +~~~~~~~~~ + +* New ``raise_last_call_error`` parameter in the client constructor to raise + the same error produced by the last failing call, rather than a generic + `ValueError`. +* New ``formatting_entities`` parameter in `client.send_message() + `, and + `client.send_file() ` + to bypass the parse mode and manually specify the formatting entities. +* New `client.get_permissions() ` + method to query a participant's permissions in a group or channel. This + request is slightly expensive in small group chats because it has to fetch + the entire chat to check just a user, so use of a cache is advised. +* `Message.click() ` now works on + normal polls! +* New ``local_addr`` parameter in the client constructor to use a specific + local network address when connecting to Telegram. +* `client.inline_query() ` now + lets you specify the chat where the query is being made from, which some + bots need to provide certain functionality. +* You can now get comments in a channel post with the ``reply_to`` parameter in + `client.iter_messages() `. + Comments are messages that "reply to" a specific channel message, hence the + name (which is consistent with how Telegram's API calls it). + +Enhancements +~~~~~~~~~~~~ + +* Updated documentation and list of known errors. +* If ``hachoir`` is available, the file metadata can now be extracted from + streams and in-memory bytes. +* The default parameters used to initialize a connection now match the format + of those used by Telegram Desktop. +* Specifying 0 retries will no longer cause the library to attempt to reconnect. +* The library should now be able to reliably download very large files. +* Global search should work more reliably now. +* Old usernames are evicted from cache, so getting entities by cached username + should now be more reliable. +* Slightly less noisy logs. +* Stability regarding transport-level errors (transport flood, authorization + key not found) should be improved. In particular, you should no longer be + getting unnecessarily logged out. +* Reconnection should no longer occur if the client gets logged out (for + example, another client revokes the session). + +Bug fixes +~~~~~~~~~ + +* In some cases, there were issues when using `events.Album + ` together with `events.Raw + `. +* For some channels, one of their channel photos would not show up in + `client.iter_profile_photos() `. +* In some cases, a request that failed to be sent would be forgotten, causing + the original caller to be "locked" forever for a response that would never + arrive. Failing requests should now consistently be automatically re-sent. +* The library should more reliably handle certain updates with "empty" data. +* Sending documents in inline queries should now work fine. +* Manually using `client.sign_up ` + should now work correctly, instead of claiming "code invalid". + +Special mention to some of the other changes in the 1.16.x series: + +* The ``thumb`` for ``download_media`` now supports both `str` and :tl:`VideoSize`. +* Thumbnails are sorted, so ``-1`` is always the largest. + + Bug Fixes (v1.16.1) =================== diff --git a/telethon/client/chats.py b/telethon/client/chats.py index a9e9c704..64cbe52b 100644 --- a/telethon/client/chats.py +++ b/telethon/client/chats.py @@ -1167,6 +1167,11 @@ class ChatMethods: """ Fetches the permissions of a user in a specific 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. diff --git a/telethon/version.py b/telethon/version.py index c8a2db55..e7b5b149 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.16.4' +__version__ = '1.17.0' From d56b27e570e9f30a901b364c8aa8430937fd4958 Mon Sep 17 00:00:00 2001 From: Qwerty-Space Date: Sun, 18 Oct 2020 20:11:59 +0100 Subject: [PATCH 12/29] Fix several minor typos (#1603) --- readthedocs/basic/quick-start.rst | 2 +- readthedocs/concepts/entities.rst | 2 +- readthedocs/misc/changelog.rst | 2 +- readthedocs/quick-references/client-reference.rst | 2 +- telethon/client/auth.py | 2 +- telethon/client/messages.py | 2 +- telethon/extensions/__init__.py | 2 +- telethon/utils.py | 2 +- telethon_generator/data/errors.csv | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/readthedocs/basic/quick-start.rst b/readthedocs/basic/quick-start.rst index b3168701..cd187c81 100644 --- a/readthedocs/basic/quick-start.rst +++ b/readthedocs/basic/quick-start.rst @@ -19,7 +19,7 @@ use these if possible. # Getting information about yourself me = await client.get_me() - # "me" is an User object. You can pretty-print + # "me" is a user object. You can pretty-print # any Telegram object with the "stringify" method: print(me.stringify()) diff --git a/readthedocs/concepts/entities.rst b/readthedocs/concepts/entities.rst index 4ee55d48..40bfac30 100644 --- a/readthedocs/concepts/entities.rst +++ b/readthedocs/concepts/entities.rst @@ -289,7 +289,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 an username? Use it! + # Does it have a username? Use it! entity = await client.get_entity(username) # Do you have a conversation open with them? Get dialogs. diff --git a/readthedocs/misc/changelog.rst b/readthedocs/misc/changelog.rst index 9f8cacfa..2a64c106 100644 --- a/readthedocs/misc/changelog.rst +++ b/readthedocs/misc/changelog.rst @@ -3806,7 +3806,7 @@ things with the ``InteractiveTelegramClient``: - **Download** any message's media (photos, documents or even contacts!). - **Receive message updates** as you talk (i.e., someone sent you a message). -It actually is an usable-enough client for your day by day. You could +It actually is a usable-enough client for your day by day. You could even add ``libnotify`` and pop, you're done! A great cli-client with desktop notifications. diff --git a/readthedocs/quick-references/client-reference.rst b/readthedocs/quick-references/client-reference.rst index 2a1553af..91b9bfe2 100644 --- a/readthedocs/quick-references/client-reference.rst +++ b/readthedocs/quick-references/client-reference.rst @@ -9,7 +9,7 @@ you may need when using Telethon. They are sorted by relevance and are not in alphabetical order. You should use this page to learn about which methods are available, and -if you need an usage example or further description of the arguments, be +if you need a usage example or further description of the arguments, be sure to follow the links. .. contents:: diff --git a/telethon/client/auth.py b/telethon/client/auth.py index 8e9cb0ef..9665262b 100644 --- a/telethon/client/auth.py +++ b/telethon/client/auth.py @@ -92,7 +92,7 @@ class AuthMethods: # Starting as a bot account await client.start(bot_token=bot_token) - # Starting as an user account + # Starting as a user account await client.start(phone) # Please enter the code you received: 12345 # Please enter your password: ******* diff --git a/telethon/client/messages.py b/telethon/client/messages.py index 7574142f..f526e318 100644 --- a/telethon/client/messages.py +++ b/telethon/client/messages.py @@ -406,7 +406,7 @@ class MessageMethods: from_user (`entity`): Only messages from this user will be returned. - This parameter will be ignored if it is not an user. + This parameter will be ignored if it is not a user. wait_time (`int`): Wait time (in seconds) between different diff --git a/telethon/extensions/__init__.py b/telethon/extensions/__init__.py index 903460b6..a3c77295 100644 --- a/telethon/extensions/__init__.py +++ b/telethon/extensions/__init__.py @@ -1,6 +1,6 @@ """ Several extensions Python is missing, such as a proper class to handle a TCP -communication with support for cancelling the operation, and an utility class +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/utils.py b/telethon/utils.py index b9fab2f1..d2dce173 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -1072,7 +1072,7 @@ def _rle_encode(string): def _decode_telegram_base64(string): """ - Decodes an url-safe base64-encoded string into its bytes + Decodes a url-safe base64-encoded string into its bytes by first adding the stripped necessary padding characters. This is the way Telegram shares binary data as strings, diff --git a/telethon_generator/data/errors.csv b/telethon_generator/data/errors.csv index 1ca79418..81813df1 100644 --- a/telethon_generator/data/errors.csv +++ b/telethon_generator/data/errors.csv @@ -297,7 +297,7 @@ TYPES_EMPTY,400,The types field is empty TYPE_CONSTRUCTOR_INVALID,,The type constructor is invalid 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 an URL that's not t.me/yourbot or your game's URL) +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) 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 USERNAME_NOT_OCCUPIED,400,The username is not in use by anyone else yet From 7ed5b4dfbea5de00f6fcce039424719a26d770e0 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Mon, 19 Oct 2020 10:48:51 +0200 Subject: [PATCH 13/29] Explain what happens when a button is pressed in the docs --- telethon/tl/custom/button.py | 42 ++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/telethon/tl/custom/button.py b/telethon/tl/custom/button.py index 1df7cdfa..134fbec7 100644 --- a/telethon/tl/custom/button.py +++ b/telethon/tl/custom/button.py @@ -20,12 +20,14 @@ 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` and `url` + You can use `inline`, `switch_inline`, `url` and `auth` together to create inline buttons (under the message). - You can use `text`, `request_location` and `request_phone` + You can use `text`, `request_location`, `request_phone` and `request_poll` together to create a reply markup (replaces the user keyboard). You can also configure the aspect of the reply with these. + The latest message with a reply markup will be the one shown to the user + (messages contain the buttons, not the chat itself). You **cannot** mix the two type of buttons together, and it will error if you try to do so. @@ -63,6 +65,13 @@ class Button: Note that the given `data` must be less or equal to 64 bytes. If more than 64 bytes are passed as data, ``ValueError`` is raised. + If you need to store more than 64 bytes, consider saving the real + data in a database and a reference to that data inside the button. + + When the user clicks this button, `events.CallbackQuery + ` will trigger with the + same data that the button contained, so that you can determine which + button was pressed. """ if not data: data = text.encode('utf-8') @@ -85,6 +94,10 @@ class Button: If ``same_peer is True`` the inline query will directly be set under the currently opened chat. Otherwise, the user will have to select a different dialog to make the query. + + When the user clicks this button, after a chat is selected, their + 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) @@ -96,6 +109,11 @@ class Button: If no `url` is given, the `text` will be used as said URL instead. You cannot detect that the user clicked this button directly. + + When the user clicks this button, a confirmation box will be shown + to the user asking whether they want to open the displayed URL unless + the domain is trusted, and once confirmed the URL will open in their + device. """ return types.KeyboardButtonUrl(text, url or text) @@ -133,6 +151,9 @@ class Button: fwd_text (`str`): The new text to show in the button if the message is forwarded. By default, the button text will be the same. + + 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( text=text, @@ -161,6 +182,12 @@ class Button: be "selective". The keyboard will be shown only to specific users. It will target users that are @mentioned in the text of the message or to the sender of the message you reply to. + + When the user clicks this button, a text message with the same text + as the button will be sent, and can be handled with `events.NewMessage + `. You cannot distinguish + between a button press and the user typing and sending exactly the + same text on their own. """ return cls(types.KeyboardButton(text), resize=resize, single_use=single_use, selective=selective) @@ -172,6 +199,10 @@ class Button: Creates a new keyboard button to request the user's location on click. ``resize``, ``single_use`` and ``selective`` are documented in `text`. + + When the user clicks this button, a confirmation box will be shown + 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), resize=resize, single_use=single_use, selective=selective) @@ -183,6 +214,10 @@ class Button: Creates a new keyboard button to request the user's phone on click. ``resize``, ``single_use`` and ``selective`` are documented in `text`. + + When the user clicks this button, a confirmation box will be shown + 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), resize=resize, single_use=single_use, selective=selective) @@ -202,6 +237,9 @@ class Button: the vote, and the pol might be multiple choice. ``resize``, ``single_use`` and ``selective`` are documented in `text`. + + 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), resize=resize, single_use=single_use, selective=selective) From f450682a226d888ab9dc5f69728abc4ecb612f7d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Wed, 21 Oct 2020 09:32:29 +0200 Subject: [PATCH 14/29] Document BOT_DOMAIN_INVALID --- telethon_generator/data/errors.csv | 1 + telethon_generator/data/methods.csv | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/telethon_generator/data/errors.csv b/telethon_generator/data/errors.csv index 81813df1..b90e9a8a 100644 --- a/telethon_generator/data/errors.csv +++ b/telethon_generator/data/errors.csv @@ -23,6 +23,7 @@ BANNED_RIGHTS_INVALID,400,"You cannot use that set of permissions in this reques BOTS_TOO_MUCH,400,There are too many bots in this chat/channel 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_DOMAIN_INVALID,400,The domain used for the auth button does not match the one configured in @BotFather BOT_GAMES_DISABLED,400,Bot games cannot be used in this type of chat BOT_GROUPS_BLOCKED,400,This bot can't be added to groups BOT_INLINE_DISABLED,400,This bot can't be used in inline mode diff --git a/telethon_generator/data/methods.csv b/telethon_generator/data/methods.csv index 5e94a49f..26e5f113 100644 --- a/telethon_generator/data/methods.csv +++ b/telethon_generator/data/methods.csv @@ -273,7 +273,7 @@ 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.sendMessage,both,AUTH_KEY_DUPLICATED 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.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,SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH messages.sendScheduledMessages,user, messages.sendVote,user,MESSAGE_POLL_CLOSED OPTION_INVALID From d9ddf8858eaa5c2e2e82b95543b5f124bb5c36e8 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Thu, 22 Oct 2020 10:13:29 +0200 Subject: [PATCH 15/29] Add missing local_addr to proxy connection, bump version Bug introduced by #1587. --- telethon/network/connection/tcpmtproxy.py | 2 +- telethon/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/telethon/network/connection/tcpmtproxy.py b/telethon/network/connection/tcpmtproxy.py index f034dfbe..69a43bce 100644 --- a/telethon/network/connection/tcpmtproxy.py +++ b/telethon/network/connection/tcpmtproxy.py @@ -95,7 +95,7 @@ class TcpMTProxy(ObfuscatedConnection): obfuscated_io = MTProxyIO # noinspection PyUnusedLocal - def __init__(self, ip, port, dc_id, *, loggers, proxy=None): + 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]) diff --git a/telethon/version.py b/telethon/version.py index e7b5b149..021d5625 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.17.0' +__version__ = '1.17.1' From e5476e6fefa6b80643e57c97befea6657ae068e6 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 23 Oct 2020 10:57:45 +0200 Subject: [PATCH 16/29] Add utils.split_text to split very large messages --- telethon/utils.py | 97 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/telethon/utils.py b/telethon/utils.py index d2dce173..18723dc7 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -20,7 +20,7 @@ from mimetypes import guess_extension from types import GeneratorType from .extensions import markdown, html -from .helpers import add_surrogate, del_surrogate +from .helpers import add_surrogate, del_surrogate, strip_text from .tl import types try: @@ -1384,6 +1384,101 @@ def decode_waveform(waveform): return bytes(result) +def split_text(text, entities, *, limit=4096, max_entities=100, split_at=(r'\n', r'\s', '.')): + """ + Split a message text and entities into multiple messages, each with their + own set of entities. This allows sending a very large message as multiple + messages while respecting the formatting. + + Arguments + text (`str`): + The message text. + + entities (List[:tl:`MessageEntity`]) + The formatting entities. + + limit (`int`): + The maximum message length of each individual message. + + max_entities (`int`): + The maximum amount of entities that will be present in each + individual message. + + split_at (Tuplel[`str`]): + The list of regular expressions that will determine where to split + the text. By default, a newline is searched. If no newline is + present, a space is searched. If no space is found, the split will + be made at any character. + + The last expression should always match a character, or else the + text will stop being splitted and the resulting text may be larger + than the limit. + + Yields + Pairs of ``(str, entities)`` with the split message. + + Example + .. code-block:: python + + from telethon import utils + from telethon.extensions import markdown + + very_long_markdown_text = "..." + text, entities = markdown.parse(very_long_markdown_text) + + for text, entities in utils.split_text(text, entities): + await client.send_message(chat, text, formatting_entities=entities) + """ + # TODO add test cases (multiple entities beyond cutoff, at cutoff, splitting at emoji) + # TODO try to optimize this a bit more? (avoid new_ent, smarter update method) + def update(ent, **updates): + kwargs = ent.to_dict() + del kwargs['_'] + kwargs.update(updates) + return ent.__class__(**kwargs) + + text = add_surrogate(text) + split_at = tuple(map(re.compile, split_at)) + + while True: + if len(entities) > max_entities: + last_ent = entities[max_entities - 1] + cur_limit = min(limit, last_ent.offset + last_ent.length) + else: + cur_limit = limit + + if len(text) <= cur_limit: + break + + for split in split_at: + for i in reversed(range(cur_limit)): + m = split.match(text, pos=i) + if m: + cur_text, new_text = text[:m.end()], text[m.end():] + cur_ent, new_ent = [], [] + for ent in entities: + if ent.offset < m.end(): + if ent.offset + ent.length > m.end(): + cur_ent.append(update(ent, length=m.end() - ent.offset)) + new_ent.append(update(ent, offset=0, length=ent.offset + ent.length - m.end())) + else: + cur_ent.append(ent) + else: + new_ent.append(update(ent, offset=ent.offset - m.end())) + + yield del_surrogate(cur_text), cur_ent + text, entities = new_text, new_ent + break + else: + continue + break + else: + # Can't find where to split, just return the remaining text and entities + break + + yield del_surrogate(text), entities + + class AsyncClassWrapper: def __init__(self, wrapped): self.wrapped = wrapped From 44e2ef6c798dccbae075a2f7f99ee657b9642a2f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 23 Oct 2020 11:02:30 +0200 Subject: [PATCH 17/29] Don't error when failing to extract response messages --- telethon/client/messageparse.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/telethon/client/messageparse.py b/telethon/client/messageparse.py index 3a0fc268..664bd5b2 100644 --- a/telethon/client/messageparse.py +++ b/telethon/client/messageparse.py @@ -193,7 +193,13 @@ class MessageParseMethods: mapping = sched_to_message opposite = id_to_message # scheduled may be treated as normal, though - random_id = request if isinstance(request, (int, list)) else request.random_id + 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 = mapping.get(random_to_id.get(random_id)) if not msg: From 1a2e09487cef26aa842e5d8dde13d5d3c1a9ec75 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 23 Oct 2020 19:02:43 +0200 Subject: [PATCH 18/29] Fix utils.get_peer not handling Self in get_messages --- telethon/client/messages.py | 4 ++-- telethon/client/users.py | 4 ++++ telethon/version.py | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/telethon/client/messages.py b/telethon/client/messages.py index f526e318..5180fa9b 100644 --- a/telethon/client/messages.py +++ b/telethon/client/messages.py @@ -290,7 +290,7 @@ class _IDsIter(RequestIter): else: r = await self.client(functions.messages.GetMessagesRequest(ids)) if self._entity: - from_id = utils.get_peer(self._entity) + from_id = await self.client._get_peer(self._entity) if isinstance(r, types.messages.MessagesNotModified): self.buffer.extend(None for _ in ids) @@ -804,7 +804,7 @@ class MessageMethods: if isinstance(result, types.UpdateShortSentMessage): message = types.Message( id=result.id, - peer_id=utils.get_peer(entity), + peer_id=await self.client.get_peer(entity), message=message, date=result.date, out=result.out, diff --git a/telethon/client/users.py b/telethon/client/users.py index 905f6f73..b2243a2e 100644 --- a/telethon/client/users.py +++ b/telethon/client/users.py @@ -463,6 +463,10 @@ class UserMethods: .format(peer) ) + 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', diff --git a/telethon/version.py b/telethon/version.py index 021d5625..71876ba6 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.17.1' +__version__ = '1.17.2' From 60c5d0d8f4c42b7200b1124bb9e0b62ab2b4b95b Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 23 Oct 2020 21:24:51 +0200 Subject: [PATCH 19/29] Fix up typo from last commit --- telethon/client/messages.py | 2 +- telethon/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/telethon/client/messages.py b/telethon/client/messages.py index 5180fa9b..f18956a7 100644 --- a/telethon/client/messages.py +++ b/telethon/client/messages.py @@ -804,7 +804,7 @@ class MessageMethods: if isinstance(result, types.UpdateShortSentMessage): message = types.Message( id=result.id, - peer_id=await self.client.get_peer(entity), + peer_id=await self.get_peer(entity), message=message, date=result.date, out=result.out, diff --git a/telethon/version.py b/telethon/version.py index 71876ba6..dad0ddf8 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.17.2' +__version__ = '1.17.3' From 62467b6318e07e95aeb7c1a7e54d15faf0b915ab Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 23 Oct 2020 21:27:39 +0200 Subject: [PATCH 20/29] Fix yet another typo Never make commits in a rush from your phone. --- telethon/client/messages.py | 2 +- telethon/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/telethon/client/messages.py b/telethon/client/messages.py index f18956a7..28202a62 100644 --- a/telethon/client/messages.py +++ b/telethon/client/messages.py @@ -804,7 +804,7 @@ class MessageMethods: if isinstance(result, types.UpdateShortSentMessage): message = types.Message( id=result.id, - peer_id=await self.get_peer(entity), + peer_id=await self._get_peer(entity), message=message, date=result.date, out=result.out, diff --git a/telethon/version.py b/telethon/version.py index dad0ddf8..9594a416 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.17.3' +__version__ = '1.17.4' From 7790307595608642c2ffc0b5d550d452b02e2c7f Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 24 Oct 2020 12:04:36 +0200 Subject: [PATCH 21/29] Remove the (out of date) .nix files from the repo --- .gitignore | 4 - default.nix | 126 --- nix/ci.nix | 59 -- nix/extended.nix | 86 -- nix/overlay.nix | 18 - nix/telethon/1.10.nix | 39 - nix/telethon/1.4.nix | 56 -- nix/telethon/1.5.nix | 60 -- nix/telethon/1.6.nix | 50 -- nix/telethon/1.7.nix | 66 -- nix/telethon/1.8.nix | 35 - nix/telethon/1.9.nix | 35 - nix/telethon/common.nix | 60 -- nix/telethon/devel.nix | 27 - .../generator-use-pathlib-to-1_4_3.patch | 819 ------------------ 15 files changed, 1540 deletions(-) delete mode 100644 default.nix delete mode 100644 nix/ci.nix delete mode 100644 nix/extended.nix delete mode 100644 nix/overlay.nix delete mode 100644 nix/telethon/1.10.nix delete mode 100644 nix/telethon/1.4.nix delete mode 100644 nix/telethon/1.5.nix delete mode 100644 nix/telethon/1.6.nix delete mode 100644 nix/telethon/1.7.nix delete mode 100644 nix/telethon/1.8.nix delete mode 100644 nix/telethon/1.9.nix delete mode 100644 nix/telethon/common.nix delete mode 100644 nix/telethon/devel.nix delete mode 100644 nix/telethon/generator-use-pathlib-to-1_4_3.patch diff --git a/.gitignore b/.gitignore index 4d8f9baa..e39a8281 100644 --- a/.gitignore +++ b/.gitignore @@ -106,7 +106,3 @@ ENV/ # Rope project settings .ropeproject - -# Nix build results -result -result-* diff --git a/default.nix b/default.nix deleted file mode 100644 index 4ae2b72f..00000000 --- a/default.nix +++ /dev/null @@ -1,126 +0,0 @@ -# A NUR-compatible package specification. -{ pkgs ? import {}, useRelease ? true }: - -rec { - # The `lib`, `modules`, and `overlay` names are special - lib = ({ pkgs }: { }) { inherit pkgs; }; # functions - modules = { }; # NixOS modules - overlays = { }; # nixpkgs overlays - - # # development - - # ## development.python-modules - - # use in a shell like - # ```nix - # ((pkgs.python3.override { - # packageOverrides = pythonPackageOverrides; - # }).withPackages (ps: [ ps.telethon ])).env - # ``` - pythonPackageOverrides = self: super: let - defaultTelethonArgs = { inherit useRelease; }; - telethonPkg = v: args: self.callPackage (./nix/telethon + "/${v}.nix") - (defaultTelethonArgs // args); - in rec { - telethon = telethon_1; - telethon-devel = self.callPackage ./nix/telethon/devel.nix { }; - - telethon_1 = telethon_1_10; - telethon_1_10 = telethon_1_10_1; - telethon_1_10_1 = telethonPkg "1.10" { version = "1.10.1"; }; - telethon_1_10_0 = telethonPkg "1.10" { version = "1.10.0"; }; - telethon_1_9 = telethon_1_9_0; - telethon_1_9_0 = telethonPkg "1.9" { version = "1.9.0"; }; - telethon_1_8 = telethon_1_8_0; - telethon_1_8_0 = telethonPkg "1.8" { version = "1.8.0"; }; - telethon_1_7 = telethon_1_7_7; - telethon_1_7_7 = telethonPkg "1.7" { version = "1.7.7"; }; - telethon_1_7_6 = telethonPkg "1.7" { version = "1.7.6"; }; - telethon_1_7_5 = telethonPkg "1.7" { version = "1.7.5"; }; - telethon_1_7_4 = telethonPkg "1.7" { version = "1.7.4"; }; - telethon_1_7_3 = telethonPkg "1.7" { version = "1.7.3"; }; - telethon_1_7_2 = telethonPkg "1.7" { version = "1.7.2"; }; - telethon_1_7_1 = telethonPkg "1.7" { version = "1.7.1"; }; - telethon_1_7_0 = telethonPkg "1.7" { version = "1.7.0"; }; - telethon_1_6 = telethon_1_6_2; - telethon_1_6_2 = telethonPkg "1.6" { version = "1.6.2"; }; - # 1.6.1.post1: hotpatch that fixed Telethon.egg-info dir perms - telethon_1_6_1 = telethonPkg "1.6" { version = "1.6.1"; }; - telethon_1_6_0 = telethonPkg "1.6" { version = "1.6.0"; }; - telethon_1_5 = telethon_1_5_5; - telethon_1_5_5 = telethonPkg "1.5" { version = "1.5.5"; }; - telethon_1_5_4 = telethonPkg "1.5" { version = "1.5.4"; }; - telethon_1_5_3 = telethonPkg "1.5" { version = "1.5.3"; }; - telethon_1_5_2 = telethonPkg "1.5" { version = "1.5.2"; }; - telethon_1_5_1 = telethonPkg "1.5" { version = "1.5.1"; }; - telethon_1_5_0 = telethonPkg "1.5" { version = "1.5.0"; }; - telethon_1_4 = telethon_1_4_3; - telethon_1_4_3 = telethonPkg "1.4" { version = "1.4.3"; }; - telethon_1_4_2 = telethonPkg "1.4" { version = "1.4.2"; }; - telethon_1_4_1 = telethonPkg "1.4" { version = "1.4.1"; }; - telethon_1_4_0 = telethonPkg "1.4" { version = "1.4.0"; }; - #telethon_1_3_0 - #telethon_1_2_0 - #telethon_1_1_1 - #telethon_1_1_0 - #telethon_1_0_4 - #telethon_1_0_3 - #telethon_1_0_2 - #telethon_1_0_1 - #telethon_1_0_0-rc1 - #telethon_1_0_0 - #telethon_0_19_1 - #telethon_0_19_0 - #telethon_0_18_3 - #telethon_0_18_2 - #telethon_0_18_1 - #telethon_0_18_0 - #telethon_0_17_4 - #telethon_0_17_3 - #telethon_0_17_2 - #telethon_0_17_1 - #telethon_0_17_0 - #telethon_0_16_2 - #telethon_0_16_1 - #telethon_0_16_0 - #telethon_0_15_5 - #telethon_0_15_4 - #telethon_0_15_3 - #telethon_0_15_2 - #telethon_0_15_1 - #telethon_0_15_0 - #telethon_0_14_2 - #telethon_0_14_1 - #telethon_0_14_0 - #telethon_0_13_6 - #telethon_0_13_5 - #telethon_0_13_4 - #telethon_0_13_3 - #telethon_0_13_2 - #telethon_0_13_1 - #telethon_0_13_0 - #telethon_0_12_2 - #telethon_0_12_1 - #telethon_0_12_0 - #telethon_0_11_5 - #telethon_0_11_4 - #telethon_0_11_3 - #telethon_0_11_2 - #telethon_0_11_1 - #telethon_0_11_0 - #telethon_0_10_1 - #telethon_0_10_0 - #telethon_0_9_1 - #telethon_0_9_0 - #telethon_0_8_0 - #telethon_0_7_1 - #telethon_0_7_0 - #telethon_0_6_0 - #telethon_0_5_0 - #telethon_0_4_0 - #telethon_0_3_0 - #telethon_0_2_0 - #telethon_0_1_0 - }; -} - diff --git a/nix/ci.nix b/nix/ci.nix deleted file mode 100644 index fe6bda54..00000000 --- a/nix/ci.nix +++ /dev/null @@ -1,59 +0,0 @@ -# This file provides all the buildable and cacheable packages and -# package outputs in you package set. These are what gets built by CI, -# so if you correctly mark packages as -# -# - broken (using `meta.broken`), -# - unfree (using `meta.license.free`), and -# - locally built (using `preferLocalBuild`) -# -# then your CI will be able to build and cache only those packages for -# which this is possible. - -{ pkgs ? import {}, enableEnvs ? false }: - -with builtins; - -let - - isReserved = n: n == "lib" || n == "overlays" || n == "modules"; - isDerivation = p: isAttrs p && p ? type && p.type == "derivation"; - isBuildable = p: !(p.meta.broken or false) && p.meta.license.free or true; - isCacheable = p: !(p.preferLocalBuild or false); - shouldRecurseForDerivations = p: - isAttrs p && p.recurseForDerivations or false; - - nameValuePair = n: v: { name = n; value = v; }; - - concatMap = builtins.concatMap or (f: xs: concatLists (map f xs)); - - flattenPkgs = s: - let - f = p: - if shouldRecurseForDerivations p then flattenPkgs p - else if isDerivation p then [p] - else []; - in - concatMap f (attrValues s); - - outputsOf = p: map (o: p.${o}) p.outputs; - - # build & test packages across Python versions - # (withPackages "distributions" are also generated for testing) - nurAttrs = import ./extended.nix { inherit pkgs enableEnvs; }; - - nurPkgs = - flattenPkgs - (listToAttrs - (map (n: nameValuePair n nurAttrs.${n}) - (filter (n: !isReserved n) - (attrNames nurAttrs)))); - -in - -rec { - buildPkgs = filter isBuildable nurPkgs; - cachePkgs = filter isCacheable buildPkgs; - - buildOutputs = concatMap outputsOf buildPkgs; - cacheOutputs = concatMap outputsOf cachePkgs; -} diff --git a/nix/extended.nix b/nix/extended.nix deleted file mode 100644 index 8121b9e5..00000000 --- a/nix/extended.nix +++ /dev/null @@ -1,86 +0,0 @@ -{ pkgs ? import { }, enableEnvs ? true, useRelease ? true }: - -# packages built against all Python versions (along with withPackages -# environments for testing) - -# to use for testing, you'll probably want a variant of: -# ```sh -# nix-shell nix/extended.nix -A telethon-devel-python37 --run "python" -# ``` - -let - inherit (pkgs.lib) attrNames attrValues concatMap head listToAttrs - mapAttrsToList optional optionals tail; - nurAttrs = import ../default.nix { inherit pkgs useRelease; }; - - pyVersions = concatMap (n: optional (pkgs ? ${n}) n) [ - "python3" - "python35" - "python36" - "python37" - # "pypy3" - # "pypy35" - # "pypy36" - # "pypy37" - ]; - - pyPkgEnvs = [ - [ "telethon" "telethon" ] - [ "telethon-devel" "telethon-devel" ] - - [ "telethon_1" "telethon_1" ] - [ "telethon_1_10" "telethon_1_10" ] - [ "telethon_1_10_1" "telethon_1_10_1" ] - [ "telethon_1_10_0" "telethon_1_10_0" ] - [ "telethon_1_9" "telethon_1_9" ] - [ "telethon_1_9_0" "telethon_1_9_0" ] - [ "telethon_1_8" "telethon_1_8" ] - [ "telethon_1_8_0" "telethon_1_8_0" ] - [ "telethon_1_7" "telethon_1_7" ] - [ "telethon_1_7_7" "telethon_1_7_7" ] - [ "telethon_1_7_6" "telethon_1_7_6" ] - [ "telethon_1_7_5" "telethon_1_7_5" ] - [ "telethon_1_7_4" "telethon_1_7_4" ] - [ "telethon_1_7_3" "telethon_1_7_3" ] - [ "telethon_1_7_2" "telethon_1_7_2" ] - [ "telethon_1_7_1" "telethon_1_7_1" ] - [ "telethon_1_7_0" "telethon_1_7_0" ] - [ "telethon_1_6" "telethon_1_6" ] - [ "telethon_1_6_2" "telethon_1_6_2" ] - [ "telethon_1_6_1" "telethon_1_6_1" ] - [ "telethon_1_6_0" "telethon_1_6_0" ] - [ "telethon_1_5" "telethon_1_5" ] - [ "telethon_1_5_5" "telethon_1_5_5" ] - [ "telethon_1_5_4" "telethon_1_5_4" ] - [ "telethon_1_5_3" "telethon_1_5_3" ] - [ "telethon_1_5_2" "telethon_1_5_2" ] - [ "telethon_1_5_1" "telethon_1_5_1" ] - [ "telethon_1_5_0" "telethon_1_5_0" ] - [ "telethon_1_4" "telethon_1_4" ] - [ "telethon_1_4_3" "telethon_1_4_3" ] - # [ "telethon_1_4_2" "telethon_1_4_2" ] - # [ "telethon_1_4_1" "telethon_1_4_1" ] - # [ "telethon_1_4_0" "telethon_1_4_0" ] - ]; - - getPkgPair = pkgs: n: let p = pkgs.${n}; in { name = n; value = p; }; - getPkgPairs = pkgs: map (getPkgPair pkgs); - pyPkgPairs = py: - concatMap (d: map (getPkgPair py.pkgs) (tail d)) pyPkgEnvs; - pyPkgEnvPair = pyNm: py: envNm: env: { - name = "${envNm}-env-${pyNm}"; - value = (py.withPackages (ps: map (pn: ps.${pn}) env)).overrideAttrs (o: { - name = "${envNm}-${py.name}-env"; - preferLocalBuild = true; - }); - }; - pyNurPairs = pyNm: py: - map ({ name, value }: { name = "${name}-${pyNm}"; inherit value; }) - (pyPkgPairs py) ++ - optionals enableEnvs - (map (d: pyPkgEnvPair pyNm py (head d) (tail d)) pyPkgEnvs); -in nurAttrs // (listToAttrs (concatMap (py: let - python = pkgs.${py}.override { - packageOverrides = nurAttrs.pythonPackageOverrides; - }; in - pyNurPairs py python) pyVersions)) diff --git a/nix/overlay.nix b/nix/overlay.nix deleted file mode 100644 index 122729de..00000000 --- a/nix/overlay.nix +++ /dev/null @@ -1,18 +0,0 @@ -# You can use this file as a nixpkgs overlay. This is useful in the -# case where you don't want to add the whole NUR namespace to your -# configuration. - -self: super: - -let - - isReserved = n: n == "lib" || n == "overlays" || n == "modules"; - nameValuePair = n: v: { name = n; value = v; }; - nurAttrs = import ./default.nix { pkgs = super; }; - -in - - builtins.listToAttrs - (map (n: nameValuePair n nurAttrs.${n}) - (builtins.filter (n: !isReserved n) - (builtins.attrNames nurAttrs))) diff --git a/nix/telethon/1.10.nix b/nix/telethon/1.10.nix deleted file mode 100644 index cf960134..00000000 --- a/nix/telethon/1.10.nix +++ /dev/null @@ -1,39 +0,0 @@ -{ lib, buildPythonPackage, pythonOlder -, fetchFromGitHub ? null, fetchPypi ? null, fetchpatch ? null -, pyaes, rsa -, version -, useRelease ? true -}: - -assert useRelease -> fetchPypi != null; -assert !useRelease -> fetchFromGitHub != null; -let - common = import ./common.nix { - inherit lib fetchFromGitHub fetchPypi fetchpatch; - }; - versions = { - "1.10.1" = { - pypiSha256 = "1ql8ai01c6v3l13lh3csh37jjkrb33gj50jyvdfi3qjn60qs2rfl"; - sourceSha256 = "1skckq4lai51p476r3shgld89x5yg5snrcrzjfxxxai00lm65cbv"; - }; - "1.10.0" = { - pypiSha256 = "1n2g2r5w44nlhn229r8kamhwjxggv16gl3jxq25bpg5y4qgrxzd8"; - sourceSha256 = "1rvrc63j6i7yr887g2csciv4zyy407yhdn4n8q2q00dkildh64qw"; - }; - }; -in buildPythonPackage rec { - pname = "telethon"; - inherit version; - - src = common.fetchTelethon { - inherit useRelease version; - versionData = versions.${version}; - }; - - propagatedBuildInputs = [ rsa pyaes ]; - - doCheck = false; # No tests available - - disabled = pythonOlder "3.5"; - meta = common.meta; -} diff --git a/nix/telethon/1.4.nix b/nix/telethon/1.4.nix deleted file mode 100644 index c2eceb7c..00000000 --- a/nix/telethon/1.4.nix +++ /dev/null @@ -1,56 +0,0 @@ -{ lib, buildPythonPackage, pythonOlder -, fetchFromGitHub ? null, fetchPypi ? null, fetchpatch ? null -, async_generator, pyaes, rsa -, version -, useRelease ? true -}: - -assert useRelease -> fetchPypi != null; -assert !useRelease -> fetchFromGitHub != null && fetchpatch != null; -let - common = import ./common.nix { - inherit lib fetchFromGitHub fetchPypi fetchpatch; - }; - versions = { - "1.4.3" = { - pypiSha256 = "1igslvhd743qy9p4kfs7lg09s8d5vhn9jhzngpv12797569p4lcj"; - sourceSha256 = "19vz0ppk7lq1dmqzf47n6h023i08pqvcwnixvm28vrijykq0z315"; - }; - "1.4.2" = { - pypiSha256 = "1f4ncyfzqj4b6zib0417r01pgnd0hb1p4aiinhlkxkmk7vy5fqfy"; - sourceSha256 = "0rsbz5kqp0d10gasadir3mgalc9aqq4fcv8xa1p7fg263f43rjl4"; - }; - "1.4.1" = { - pypiSha256 = "1n0jhdqflinyamzy5krnww7hc0s7pw9yfck1p7816pdbgir74qsw"; - sourceSha256 = "07q48gw4ry3wf9yzi6kf8lw3b23a0dvk9r8sabpxwrlqy7gnksxx"; - }; - "1.4.0" = { - version = "1.4"; - pypiSha256 = "1g7rznwmj87n9k86zby9i75h570hm84izrv0srhsmxi52pjan1ml"; - sourceSha256 = "14nv86yrj01wmlj5cfg6iq5w03ssl67av1arfy9mq1935mly5nly"; - }; - }; -in buildPythonPackage rec { - pname = "telethon"; - inherit version; - - src = common.fetchTelethon { - inherit useRelease version; - versionData = versions.${version}; - }; - patches = lib.optionals (!useRelease) [ - (if (lib.versionOlder version "1.4.3") then - common.patches.generator-use-pathlib-to-1_4_3 - else - common.patches.generator-use-pathlib-from-1_4_3-to-1_5_0) - common.patches.generator-use-pathlib-open-to-1_5_3 - common.patches.sort-generated-tlobjects-to-1_7_1 - ]; - - propagatedBuildInputs = [ async_generator rsa pyaes ]; - - doCheck = false; # No tests available - - disabled = pythonOlder "3.5"; - meta = common.meta; -} diff --git a/nix/telethon/1.5.nix b/nix/telethon/1.5.nix deleted file mode 100644 index 3d376da0..00000000 --- a/nix/telethon/1.5.nix +++ /dev/null @@ -1,60 +0,0 @@ -{ lib, buildPythonPackage, pythonOlder -, fetchFromGitHub ? null, fetchPypi ? null, fetchpatch ? null -, async_generator, pyaes, rsa -, version -, useRelease ? true -}: - -assert useRelease -> fetchPypi != null; -assert !useRelease -> fetchFromGitHub != null && fetchpatch != null; -let - common = import ./common.nix { - inherit lib fetchFromGitHub fetchPypi fetchpatch; - }; - versions = { - "1.5.5" = { - pypiSha256 = "1qpc4vc3lidhlp1c7521nxizjr6y5c3l9x41knqv02x8n3l9knxa"; - sourceSha256 = "1x5niscjbrg5a0cg261z6awln57v3nn8si5j58vhsnckws2c48a5"; - }; - "1.5.4" = { - pypiSha256 = "1kjqi3wy4hswsf3vmrjg7z5c3f9wpdfk4wz1yfsqmj9ppwllkjsj"; - sourceSha256 = "0rmp9zk7a354nb39c01mjcrhi2j6v9im40xmdcvmizx990vlv476"; - }; - "1.5.3" = { - pypiSha256 = "11xd5ni0chzsfny0vwwqyh37mvmrwrk2bmkhwp1ipbxyis8jjjia"; - sourceSha256 = "1l3i6wx3fgcy3vmr75qdbv5fvc5qnk0j47hv7jszsqq9rvqvz2xs"; - }; - "1.5.2" = { - pypiSha256 = "0ymv6l9xn41sgpkilqkivwbjna89m43i0a728lak2cppp7i1i1h7"; - sourceSha256 = "0gnqvlhh3qyvibl7icn6774rshlx1nnhb5f78609da44743lyv17"; - }; - "1.5.1" = { - pypiSha256 = "1ypxpsfj814gzln4fl7z17l1l6q0bzd5p1ivas85yim3a992ixww"; - sourceSha256 = "15w5nshvmj8hgqdcbpw0fjcf1cspaci8dldm9ml1pmijw7zgmpdg"; - }; - "1.5.0" = { - version = "1.5"; - pypiSha256 = "1kzkzcxyz7adjzvm2ml9faz2c5yx469j211yvi5xfvjwp58ic2jc"; - sourceSha256 = "12232d3xfv0bbykk9xaxpxsr3656ywjx4ra1q5q99rpp6wv438n1"; - }; - }; -in buildPythonPackage rec { - pname = "telethon"; - inherit version; - - src = common.fetchTelethon { - inherit useRelease version; - versionData = versions.${version}; - }; - patches = lib.optionals (!useRelease) ([ - common.patches.sort-generated-tlobjects-to-1_7_1 - ] ++ lib.optional (lib.versionOlder version "1.5.3") - common.patches.generator-use-pathlib-open-to-1_5_3); - - propagatedBuildInputs = [ async_generator rsa pyaes ]; - - doCheck = false; # No tests available - - disabled = pythonOlder "3.5"; - meta = common.meta; -} diff --git a/nix/telethon/1.6.nix b/nix/telethon/1.6.nix deleted file mode 100644 index b8033194..00000000 --- a/nix/telethon/1.6.nix +++ /dev/null @@ -1,50 +0,0 @@ -{ lib, buildPythonPackage, pythonOlder -, fetchFromGitHub ? null, fetchPypi ? null, fetchpatch ? null -, pyaes, rsa -, version -, useRelease ? true -}: - -assert useRelease -> fetchPypi != null; -assert !useRelease -> fetchFromGitHub != null && fetchpatch != null; -let - common = import ./common.nix { - inherit lib fetchFromGitHub fetchPypi fetchpatch; - }; - versions = { - "1.6.2" = { - pypiSha256 = "074h5gj0c330rb1nxzpqm31fp1vw7calh1cdkapbjx90j769iz18"; - sourceSha256 = "1daqlb4sva5qkljzbjr8xvjfgp7bdcrl2li1i4434za6a0isgd3j"; - }; - "1.6.1" = { - # hotpatch with missing .pyc files and fixed Telethon.egg-info perms - pypiVersion = "1.6.1.post1"; - pypiSha256 = "17s1qp69bbj6jniam9wbcpaj60ah56sjw0q3kr8ca28y17s88si7"; - # pypiVersion = "1.6.1"; - # pypiSha256 = "036lhr1jr79np74c6ih51c4pjy828r3lvwcq07q5wynyjprm1qbz"; - sourceSha256 = "1hk1bpnk51rpsifb67s31c2qph5hmw28i2vgh97i4i56vynx2yxz"; - }; - "1.6.0" = { - version = "1.6"; - pypiSha256 = "06prmld9068zcm9rfmq3rpq1szw72c6dkxl62b035i9w8wdpvg0m"; - sourceSha256 = "0qk14mrnvv9a043ik0y2w6q97l83abvbvn441zn2jl00w4ykfqrh"; - }; - }; -in buildPythonPackage rec { - pname = "telethon"; - inherit version; - - src = common.fetchTelethon { - inherit useRelease version; - versionData = versions.${version}; - }; - patches = lib.optional (!useRelease) - common.patches.sort-generated-tlobjects-to-1_7_1; - - propagatedBuildInputs = [ rsa pyaes ]; - - doCheck = false; # No tests available - - disabled = pythonOlder "3.5"; - meta = common.meta; -} diff --git a/nix/telethon/1.7.nix b/nix/telethon/1.7.nix deleted file mode 100644 index 28768111..00000000 --- a/nix/telethon/1.7.nix +++ /dev/null @@ -1,66 +0,0 @@ -{ lib, buildPythonPackage, pythonOlder -, fetchFromGitHub ? null, fetchPypi ? null, fetchpatch ? null -, pyaes, rsa -, version -, useRelease ? true -}: - -assert useRelease -> fetchPypi != null; -assert !useRelease -> fetchFromGitHub != null; -let - common = import ./common.nix { - inherit lib fetchFromGitHub fetchPypi fetchpatch; - }; - versions = { - "1.7.7" = { - pypiSha256 = "0mgpihjc7g4gfrq57srripdavxbsgivn4qsjanv3yds5drskciv0"; - sourceSha256 = "08c3iakd7fyacc79pg8hyzpa6zx3gbp7xivi10af34zj775lp2pi"; - }; - "1.7.6" = { - pypiSha256 = "192xda98685s3hmz7ircxpsn7yq913y0r1kmqrsav90m4g4djn4j"; - sourceSha256 = "1ss2pfpd3hby25g9ighbr7ccp66awfzda4srsnvr9s6i28har6ag"; - }; - "1.7.5" = { - pypiSha256 = "0i5s7ahicw5k0s1i7pi26vc6rp6ppr1gr848sa61yh3qqa4c0qnr"; - sourceSha256 = "1rssh0l466h9y6v0z095c9aa63nz9im7gg5771jjj5w70mkpm5w6"; - }; - "1.7.4" = { - pypiSha256 = "1qpc9f1y559zdwz59qqz4hbf1mrynjjbcg357nzaa2x5a2q4lz0s"; - sourceSha256 = "1q43lwfp67q4skfcrb6sdlnjw4ajrpizf08fd9wjrw521kkd8g4y"; - }; - "1.7.3" = { - pypiSha256 = "0s8qmsarlfgpb0k3w50siv354hpa7b1dnrjjd0iqz7vc5bc7ni84"; - sourceSha256 = "0c393smp1qm8kk39r0k31p74p89qzvjdjxq4bxq75h07a1yqbs8x"; - }; - "1.7.2" = { - pypiSha256 = "0465dwikhpbka2sj1g952rac03jkixq497gbmmyx2i9xb594db27"; - sourceSha256 = "1gw09zbaqvn074skwjhmm4yp8p75rw9njwjbkcfvqb4gr6dg8wpq"; - }; - "1.7.1" = { - pypiSha256 = "186z6imf7zqy8vf4yv2w2kxpd7lxmfppa1qi8nxjdgq8rz7wbglf"; - sourceSha256 = "05mpqfj4w5qxyl1ai5p0f31pkagz55xxh8060r8y9i3d44j9bn1c"; - }; - "1.7.0" = { - version = "1.7"; - pypiSha256 = "06cqb121k2y0h3x7gvckyvbsn97wc1a25pghinxz2vb7vg8wwxvw"; - sourceSha256 = "0myx32hqax71ijfw6ksxvk27cb6x06kbz8jb7ib9d1cayr2viir6"; - }; - }; -in buildPythonPackage rec { - pname = "telethon"; - inherit version; - - src = common.fetchTelethon { - inherit useRelease version; - versionData = versions.${version}; - }; - patches = lib.optional (!useRelease && lib.versionOlder version "1.7.1") - common.patches.sort-generated-tlobjects-to-1_7_1; - - propagatedBuildInputs = [ rsa pyaes ]; - - doCheck = false; # No tests available - - disabled = pythonOlder "3.5"; - meta = common.meta; -} diff --git a/nix/telethon/1.8.nix b/nix/telethon/1.8.nix deleted file mode 100644 index 9f52156b..00000000 --- a/nix/telethon/1.8.nix +++ /dev/null @@ -1,35 +0,0 @@ -{ lib, buildPythonPackage, pythonOlder -, fetchFromGitHub ? null, fetchPypi ? null, fetchpatch ? null -, pyaes, rsa -, version -, useRelease ? true -}: - -assert useRelease -> fetchPypi != null; -assert !useRelease -> fetchFromGitHub != null; -let - common = import ./common.nix { - inherit lib fetchFromGitHub fetchPypi fetchpatch; - }; - versions = { - "1.8.0" = { - pypiSha256 = "099br8ldjrfzwipv7g202lnjghmqj79j6gicgx11s0vawb5mb3vf"; - sourceSha256 = "1q5mcijmjw2m2v3ilw28xnavmcdck5md0k98kwnz0kyx4iqckcv0"; - }; - }; -in buildPythonPackage rec { - pname = "telethon"; - inherit version; - - src = common.fetchTelethon { - inherit useRelease version; - versionData = versions.${version}; - }; - - propagatedBuildInputs = [ rsa pyaes ]; - - doCheck = false; # No tests available - - disabled = pythonOlder "3.5"; - meta = common.meta; -} diff --git a/nix/telethon/1.9.nix b/nix/telethon/1.9.nix deleted file mode 100644 index 04ada22e..00000000 --- a/nix/telethon/1.9.nix +++ /dev/null @@ -1,35 +0,0 @@ -{ lib, buildPythonPackage, pythonOlder -, fetchFromGitHub ? null, fetchPypi ? null, fetchpatch ? null -, pyaes, rsa -, version -, useRelease ? true -}: - -assert useRelease -> fetchPypi != null; -assert !useRelease -> fetchFromGitHub != null; -let - common = import ./common.nix { - inherit lib fetchFromGitHub fetchPypi fetchpatch; - }; - versions = { - "1.9.0" = { - pypiSha256 = "1p4y4qd1ndzi1lg4fhnvq1rqz7611yrwnwwvzh63aazfpzaplyd8"; - sourceSha256 = "1g6khxc7mvm3q8rqksw9dwn4l2w8wzvr3zb74n2lb7g5ilpxsadd"; - }; - }; -in buildPythonPackage rec { - pname = "telethon"; - inherit version; - - src = common.fetchTelethon { - inherit useRelease version; - versionData = versions.${version}; - }; - - propagatedBuildInputs = [ rsa pyaes ]; - - doCheck = false; # No tests available - - disabled = pythonOlder "3.5"; - meta = common.meta; -} diff --git a/nix/telethon/common.nix b/nix/telethon/common.nix deleted file mode 100644 index 96b115c4..00000000 --- a/nix/telethon/common.nix +++ /dev/null @@ -1,60 +0,0 @@ -{ lib, fetchFromGitHub ? null, fetchPypi ? null, fetchpatch ? null }: - -rec { - fetchTelethon = { useRelease, version, versionData }: - if useRelease then assert versionData.pypiSha256 != null; fetchPypi { - pname = "Telethon"; - version = versionData.pypiVersion or (versionData.version or version); - sha256 = versionData.pypiSha256; - } else assert versionData.sourceSha256 != null; fetchFromGitHub { - owner = "LonamiWebs"; - repo = "Telethon"; - rev = versionData.rev or "v${versionData.version or version}"; - sha256 = versionData.sourceSha256; - }; - - fetchpatchTelethon = { rev, ... } @ args: - fetchpatch ({ - url = "https://github.com/LonamiWebs/Telethon/commit/${rev}.patch"; - } // (builtins.removeAttrs args [ "rev" ])); - - # sorted by name, then by logical version range - patches = rec { - generator-use-pathlib-to-1_4_3 = ./generator-use-pathlib-to-1_4_3.patch; - generator-use-pathlib-from-1_4_3-to-1_5_0 = [ - (fetchpatchTelethon { - rev = "e71c556ca71aec11166dc66f949a05e700aeb24f"; - sha256 = "058phfaggf22j0cjpy9j17y63zgd9m8j4qf7ldsg0jqm1vrym76w"; - }) - (fetchpatchTelethon { - rev = "8224e5aabf18bb31c6af8c460c38ced11756f080"; - sha256 = "0x3xfkld4d2kc0a1a8ldxy85pi57zaipq3b401b16r6rzbi4sh1j"; - }) - (fetchpatchTelethon { - rev = "aefa429236d28ae68bec4e4ef9f12d13f647dfe6"; - sha256 = "043hks8hg5sli1amfv5453h831nwy4dgyw8xr4xxfaxh74754icx"; - }) - ]; - generator-use-pathlib-open-to-1_5_3 = fetchpatchTelethon { - rev = "b57e3e3e0a752903fe7d539fb87787ec6712a3d9"; - sha256 = "1rl3lkwfi3h62ppzglrmz13zfai8i8cchzqgbjccr4l7nzh1n6nq"; - }; - sort-generated-tlobjects-to-1_7_1 = fetchpatchTelethon { - rev = "08f8aa3c526c043c107ec1b489b89c011555722f"; - sha256 = "1lkvvjzhm9jfrxpm4hbvvysz5f3qi0v4f7vqnfmrzawl73s8qk80"; - }; - }; - - meta = let inherit (lib) licenses maintainers; in { - description = "Full-featured Telegram client library for Python 3"; - fullDescription = '' - Telegram is a popular messaging application. This library is meant to - make it easy for you to write Python programs that can interact with - Telegram. Think of it as a wrapper that has already done the heavy job - for you, so you can focus on developing an application. - ''; - homepage = https://github.com/LonamiWebs/Telethon; - license = licenses.mit; - maintainers = [ maintainers.bb010g maintainers.nyanloutre ]; - }; -} diff --git a/nix/telethon/devel.nix b/nix/telethon/devel.nix deleted file mode 100644 index 8d5014b1..00000000 --- a/nix/telethon/devel.nix +++ /dev/null @@ -1,27 +0,0 @@ -{ lib, buildPythonPackage, nix-gitignore, pythonOlder -, async_generator, pyaes, rsa -}: - -let - common = import ./common.nix { inherit lib; }; -in buildPythonPackage rec { - pname = "telethon"; - # If pinning to a specific commit, use the following output instead: - # ```sh - # TZ=UTC git show -s --format=format:%cd --date=short-local - # ``` - version = "HEAD"; - - src = nix-gitignore.gitignoreSource '' - /.git - /default.nix - /nix - '' ../..; - - propagatedBuildInputs = [ async_generator rsa pyaes ]; - - doCheck = false; # No tests available - - disabled = pythonOlder "3.5"; - meta = common.meta; -} diff --git a/nix/telethon/generator-use-pathlib-to-1_4_3.patch b/nix/telethon/generator-use-pathlib-to-1_4_3.patch deleted file mode 100644 index ec69338d..00000000 --- a/nix/telethon/generator-use-pathlib-to-1_4_3.patch +++ /dev/null @@ -1,819 +0,0 @@ ---- a/setup.py -+++ b/setup.py -@@ -12,10 +12,11 @@ - - import itertools - import json --import os - import re - import shutil --from codecs import open -+from os import chdir -+from pathlib import Path -+from subprocess import run - from sys import argv - - from setuptools import find_packages, setup -@@ -29,30 +30,29 @@ - self.original = None - - def __enter__(self): -- self.original = os.path.abspath(os.path.curdir) -- os.chdir(os.path.abspath(os.path.dirname(__file__))) -+ self.original = Path('.') -+ chdir(str(Path(__file__).parent)) - return self - - def __exit__(self, *args): -- os.chdir(self.original) -+ chdir(str(self.original)) - - --GENERATOR_DIR = 'telethon_generator' --LIBRARY_DIR = 'telethon' -+GENERATOR_DIR = Path('telethon_generator') -+LIBRARY_DIR = Path('telethon') - --ERRORS_IN_JSON = os.path.join(GENERATOR_DIR, 'data', 'errors.json') --ERRORS_IN_DESC = os.path.join(GENERATOR_DIR, 'data', 'error_descriptions') --ERRORS_OUT = os.path.join(LIBRARY_DIR, 'errors', 'rpcerrorlist.py') -+ERRORS_IN_JSON = GENERATOR_DIR / 'data/errors.json' -+ERRORS_IN_DESC = GENERATOR_DIR / 'data/error_descriptions' -+ERRORS_OUT = LIBRARY_DIR / 'errors/rpcerrorlist.py' - --INVALID_BM_IN = os.path.join(GENERATOR_DIR, 'data', 'invalid_bot_methods.json') -+INVALID_BM_IN = GENERATOR_DIR / 'data/invalid_bot_methods.json' - --TLOBJECT_IN_CORE_TL = os.path.join(GENERATOR_DIR, 'data', 'mtproto_api.tl') --TLOBJECT_IN_TL = os.path.join(GENERATOR_DIR, 'data', 'telegram_api.tl') --TLOBJECT_OUT = os.path.join(LIBRARY_DIR, 'tl') -+TLOBJECT_IN_TLS = [Path(x) for x in GENERATOR_DIR.glob('data/*.tl')] -+TLOBJECT_OUT = LIBRARY_DIR / 'tl' - IMPORT_DEPTH = 2 - --DOCS_IN_RES = os.path.join(GENERATOR_DIR, 'data', 'html') --DOCS_OUT = 'docs' -+DOCS_IN_RES = GENERATOR_DIR / 'data/html' -+DOCS_OUT = Path('docs') - - - def generate(which): -@@ -60,15 +60,12 @@ - from telethon_generator.generators import\ - generate_errors, generate_tlobjects, generate_docs, clean_tlobjects - -- # Older Python versions open the file as bytes instead (3.4.2) -- with open(INVALID_BM_IN, 'r') as f: -+ with INVALID_BM_IN.open('r') as f: - invalid_bot_methods = set(json.load(f)) -- -- layer = find_layer(TLOBJECT_IN_TL) -+ layer = next(filter(None, map(find_layer, TLOBJECT_IN_TLS))) - errors = list(parse_errors(ERRORS_IN_JSON, ERRORS_IN_DESC)) -- tlobjects = list(itertools.chain( -- parse_tl(TLOBJECT_IN_CORE_TL, layer, invalid_bot_methods), -- parse_tl(TLOBJECT_IN_TL, layer, invalid_bot_methods))) -+ tlobjects = list(itertools.chain(*( -+ parse_tl(file, layer, invalid_bot_methods) for file in TLOBJECT_IN_TLS))) - - if not which: - which.extend(('tl', 'errors')) -@@ -96,30 +93,29 @@ - which.remove('errors') - print(action, 'RPCErrors...') - if clean: -- if os.path.isfile(ERRORS_OUT): -- os.remove(ERRORS_OUT) -+ if ERRORS_OUT.is_file(): -+ ERRORS_OUT.unlink() - else: -- with open(ERRORS_OUT, 'w', encoding='utf-8') as file: -+ with ERRORS_OUT.open('w') as file: - generate_errors(errors, file) - - if 'docs' in which: - which.remove('docs') - print(action, 'documentation...') - if clean: -- if os.path.isdir(DOCS_OUT): -- shutil.rmtree(DOCS_OUT) -+ if DOCS_OUT.is_dir(): -+ shutil.rmtree(str(DOCS_OUT)) - else: - generate_docs(tlobjects, methods, layer, DOCS_IN_RES, DOCS_OUT) - - if 'json' in which: - which.remove('json') - print(action, 'JSON schema...') -- mtproto = 'mtproto_api.json' -- telegram = 'telegram_api.json' -+ json_files = [x.with_suffix('.json') for x in TLOBJECT_IN_TLS] - if clean: -- for x in (mtproto, telegram): -- if os.path.isfile(x): -- os.remove(x) -+ for file in json_files: -+ if file.is_file(): -+ file.unlink() - else: - def gen_json(fin, fout): - methods = [] -@@ -131,8 +130,8 @@ - with open(fout, 'w') as f: - json.dump(what, f, indent=2) - -- gen_json(TLOBJECT_IN_CORE_TL, mtproto) -- gen_json(TLOBJECT_IN_TL, telegram) -+ for fin, fout in zip(TLOBJECT_IN_TLS, json_files): -+ gen_json(fin, fout) - - if which: - print('The following items were not understood:', which) -@@ -156,22 +155,17 @@ - print('Packaging for PyPi aborted, importing the module failed.') - return - -- # Need python3.5 or higher, but Telethon is supposed to support 3.x -- # Place it here since noone should be running ./setup.py pypi anyway -- from subprocess import run -- from shutil import rmtree -- - for x in ('build', 'dist', 'Telethon.egg-info'): -- rmtree(x, ignore_errors=True) -+ shutil.rmtree(x, ignore_errors=True) - run('python3 setup.py sdist', shell=True) - run('python3 setup.py bdist_wheel', shell=True) - run('twine upload dist/*', shell=True) - for x in ('build', 'dist', 'Telethon.egg-info'): -- rmtree(x, ignore_errors=True) -+ shutil.rmtree(x, ignore_errors=True) - - else: - # e.g. install from GitHub -- if os.path.isdir(GENERATOR_DIR): -+ if GENERATOR_DIR.is_dir(): - generate(['tl', 'errors']) - - # Get the long description from the README file ---- a/telethon_generator/docswriter.py -+++ b/telethon_generator/docswriter.py -@@ -2,0 +2,0 @@ - - - class DocsWriter: -- """Utility class used to write the HTML files used on the documentation""" -- def __init__(self, filename, type_to_path): -- """Initializes the writer to the specified output file, -- creating the parent directories when used if required. -- -- 'type_to_path_function' should be a function which, given a type -- name and a named argument relative_to, returns the file path for -- the specified type, relative to the given filename -+ """ -+ Utility class used to write the HTML files used on the documentation. -+ """ -+ def __init__(self, root, filename, type_to_path): - """ -+ Initializes the writer to the specified output file, -+ creating the parent directories when used if required. -+ """ -+ self.root = root - self.filename = filename -+ self._parent = str(self.filename.parent) - self.handle = None -+ self.title = '' - - # Should be set before calling adding items to the menu - self.menu_separator_tag = None - -- # Utility functions TODO There must be a better way -- self.type_to_path = lambda t: type_to_path( -- t, relative_to=self.filename -- ) -+ # Utility functions -+ self.type_to_path = lambda t: self._rel(type_to_path(t)) - - # Control signals - self.menu_began = False -@@ -30,11 +30,20 @@ - self.write_copy_script = False - self._script = '' - -+ def _rel(self, path): -+ """ -+ Get the relative path for the given path from the current -+ file by working around https://bugs.python.org/issue20012. -+ """ -+ return os.path.relpath(str(path), self._parent) -+ - # High level writing -- def write_head(self, title, relative_css_path, default_css): -+ def write_head(self, title, css_path, default_css): - """Writes the head part for the generated document, - with the given title and CSS - """ -+ # -+ self.title = title - self.write( - ''' - -@@ -54,17 +63,17 @@ - -
''', - title=title, -- rel_css=relative_css_path.rstrip('/'), -+ rel_css=self._rel(css_path), - def_css=default_css - ) - -- def set_menu_separator(self, relative_image_path): -+ def set_menu_separator(self, img): - """Sets the menu separator. - Must be called before adding entries to the menu - """ -- if relative_image_path: -- self.menu_separator_tag = \ -- '/'.format(relative_image_path) -+ if img: -+ self.menu_separator_tag = '/'.format( -+ self._rel(img)) - else: - self.menu_separator_tag = None - -@@ -80,7 +89,7 @@ - - self.write('
  • ') - if link: -- self.write('', link) -+ self.write('', self._rel(link)) - - # Write the real menu entry text - self.write(name) -@@ -210,7 +219,7 @@ - if bold: - self.write('') - if link: -- self.write('', link) -+ self.write('', self._rel(link)) - - # Finally write the real table data, the given text - self.write(text) -@@ -278,10 +287,7 @@ - # With block - def __enter__(self): - # Sanity check -- parent = os.path.dirname(self.filename) -- if parent: -- os.makedirs(parent, exist_ok=True) -- -+ self.filename.parent.mkdir(parents=True, exist_ok=True) - self.handle = open(self.filename, 'w', encoding='utf-8') - return self - ---- a/telethon_generator/generators/docs.py -+++ b/telethon_generator/generators/docs.py -@@ -1,7 +1,6 @@ - #!/usr/bin/env python3 --import csv - import functools --import os - import re - import shutil - from collections import defaultdict -+from pathlib import Path - - from ..docswriter import DocsWriter - from ..parsers import TLObject, Usability -@@ -35,41 +34,33 @@ - - def _get_create_path_for(root, tlobject, make=True): - """Creates and returns the path for the given TLObject at root.""" -- out_dir = 'methods' if tlobject.is_function else 'constructors' -+ # TODO Can we pre-create all required directories? -+ out_dir = root / ('methods' if tlobject.is_function else 'constructors') - if tlobject.namespace: -- out_dir = os.path.join(out_dir, tlobject.namespace) -+ out_dir /= tlobject.namespace - -- out_dir = os.path.join(root, out_dir) - if make: -- os.makedirs(out_dir, exist_ok=True) -- return os.path.join(out_dir, _get_file_name(tlobject)) -+ out_dir.mkdir(parents=True, exist_ok=True) - -+ return out_dir / _get_file_name(tlobject) - --def _get_path_for_type(root, type_, relative_to='.'): -+ -+def _get_path_for_type(type_): - """Similar to `_get_create_path_for` but for only type names.""" - if type_.lower() in CORE_TYPES: -- path = 'index.html#%s' % type_.lower() -+ return Path('index.html#%s' % type_.lower()) - elif '.' in type_: - namespace, name = type_.split('.') -- path = 'types/%s/%s' % (namespace, _get_file_name(name)) -+ return Path('types', namespace, _get_file_name(name)) - else: -- path = 'types/%s' % _get_file_name(type_) -- -- return _get_relative_path(os.path.join(root, path), relative_to) -- -- --def _get_relative_path(destination, relative_to, folder=False): -- """Return the relative path to destination from relative_to.""" -- if not folder: -- relative_to = os.path.dirname(relative_to) -- -- return os.path.relpath(destination, start=relative_to) -+ return Path('types', _get_file_name(type_)) - - - def _find_title(html_file): - """Finds the for the given HTML file, or (Unknown).""" -- with open(html_file, 'r') as fp: -- for line in fp: -+ # TODO Is it necessary to read files like this? -+ with html_file.open() as f: -+ for line in f: - if '<title>' in line: - # + 7 to skip len('<title>') - return line[line.index('<title>') + 7:line.index('')] -@@ -77,25 +68,27 @@ - return '(Unknown)' - - --def _build_menu(docs, filename, root, relative_main_index): -- """Builds the menu using the given DocumentWriter up to 'filename', -- which must be a file (it cannot be a directory)""" -- filename = _get_relative_path(filename, root) -- docs.add_menu('API', relative_main_index) -- -- items = filename.split('/') -- for i in range(len(items) - 1): -- item = items[i] -- link = '../' * (len(items) - (i + 2)) -- link += 'index.html' -- docs.add_menu(item.title(), link=link) -+def _build_menu(docs): -+ """ -+ Builds the menu used for the current ``DocumentWriter``. -+ """ -+ -+ paths = [] -+ current = docs.filename -+ while current != docs.root: -+ current = current.parent -+ paths.append(current) -+ -+ for path in reversed(paths): -+ docs.add_menu(path.stem.title(), link=path / 'index.html') -+ -+ if docs.filename.stem != 'index': -+ docs.add_menu(docs.title, link=docs.filename) - -- if items[-1] != 'index.html': -- docs.add_menu(os.path.splitext(items[-1])[0]) - docs.end_menu() - - --def _generate_index(folder, original_paths, root, -+def _generate_index(root, folder, paths, - bots_index=False, bots_index_paths=()): - """Generates the index file for the specified folder""" - # Determine the namespaces listed here (as sub folders) -@@ -105,38 +98,24 @@ - INDEX = 'index.html' - BOT_INDEX = 'botindex.html' - -- if not bots_index: -- for item in os.listdir(folder): -- if os.path.isdir(os.path.join(folder, item)): -- namespaces.append(item) -- elif item not in (INDEX, BOT_INDEX): -- files.append(item) -- else: -- # bots_index_paths should be a list of "namespace/method.html" -- # or "method.html" -- for item in bots_index_paths: -- dirname = os.path.dirname(item) -- if dirname and dirname not in namespaces: -- namespaces.append(dirname) -- elif not dirname and item not in (INDEX, BOT_INDEX): -- files.append(item) -- -- paths = {k: _get_relative_path(v, folder, folder=True) -- for k, v in original_paths.items()} -+ for item in (bots_index_paths or folder.iterdir()): -+ if item.is_dir(): -+ namespaces.append(item) -+ elif item.name not in (INDEX, BOT_INDEX): -+ files.append(item) - - # Now that everything is setup, write the index.html file -- filename = os.path.join(folder, BOT_INDEX if bots_index else INDEX) -- with DocsWriter(filename, type_to_path=_get_path_for_type) as docs: -+ filename = folder / (BOT_INDEX if bots_index else INDEX) -+ with DocsWriter(root, filename, _get_path_for_type) as docs: - # Title should be the current folder name -- docs.write_head(folder.title(), -- relative_css_path=paths['css'], -- default_css=original_paths['default_css']) -+ docs.write_head(str(folder).title(), -+ css_path=paths['css'], -+ default_css=paths['default_css']) - - docs.set_menu_separator(paths['arrow']) -- _build_menu(docs, filename, root, -- relative_main_index=paths['index_all']) -+ _build_menu(docs) -+ docs.write_title(str(filename.parent.relative_to(root)).title()) - -- docs.write_title(_get_relative_path(folder, root, folder=True).title()) - if bots_index: - docs.write_text('These are the methods that you may be able to ' - 'use as a bot. Click here to ' -@@ -153,24 +132,22 @@ - namespace_paths = [] - if bots_index: - for item in bots_index_paths: -- if os.path.dirname(item) == namespace: -- namespace_paths.append(os.path.basename(item)) -- _generate_index(os.path.join(folder, namespace), -- original_paths, root, -+ if item.parent == namespace: -+ namespace_paths.append(item) -+ -+ _generate_index(root, namespace, paths, - bots_index, namespace_paths) -- if bots_index: -- docs.add_row(namespace.title(), -- link=os.path.join(namespace, BOT_INDEX)) -- else: -- docs.add_row(namespace.title(), -- link=os.path.join(namespace, INDEX)) -+ -+ docs.add_row( -+ namespace.stem.title(), -+ link=namespace / (BOT_INDEX if bots_index else INDEX)) - - docs.end_table() - - docs.write_title('Available items') - docs.begin_table(2) - -- files = [(f, _find_title(os.path.join(folder, f))) for f in files] -+ files = [(f, _find_title(f)) for f in files] - files.sort(key=lambda t: t[1]) - - for file, title in files: -@@ -231,7 +208,7 @@ - )) - - --def _write_html_pages(tlobjects, methods, layer, input_res, output_dir): -+def _write_html_pages(root, tlobjects, methods, layer, input_res): - """ - Generates the documentation HTML files from from ``scheme.tl`` - to ``/methods`` and ``/constructors``, etc. -@@ -239,21 +216,18 @@ - # Save 'Type: [Constructors]' for use in both: - # * Seeing the return type or constructors belonging to the same type. - # * Generating the types documentation, showing available constructors. -- original_paths = { -- 'css': 'css', -- 'arrow': 'img/arrow.svg', -- 'search.js': 'js/search.js', -- '404': '404.html', -- 'index_all': 'index.html', -- 'bot_index': 'botindex.html', -- 'index_types': 'types/index.html', -- 'index_methods': 'methods/index.html', -- 'index_constructors': 'constructors/index.html' -- } -- original_paths = {k: os.path.join(output_dir, v) -- for k, v in original_paths.items()} -- -- original_paths['default_css'] = 'light' # docs..css, local path -+ paths = {k: root / v for k, v in ( -+ ('css', 'css'), -+ ('arrow', 'img/arrow.svg'), -+ ('search.js', 'js/search.js'), -+ ('404', '404.html'), -+ ('index_all', 'index.html'), -+ ('bot_index', 'botindex.html'), -+ ('index_types', 'types/index.html'), -+ ('index_methods', 'methods/index.html'), -+ ('index_constructors', 'constructors/index.html') -+ )} -+ paths['default_css'] = 'light' # docs..css, local path - type_to_constructors = defaultdict(list) - type_to_functions = defaultdict(list) - for tlobject in tlobjects: -@@ -266,24 +240,20 @@ - methods = {m.name: m for m in methods} - - # Since the output directory is needed everywhere partially apply it now -- create_path_for = functools.partial(_get_create_path_for, output_dir) -- path_for_type = functools.partial(_get_path_for_type, output_dir) -+ create_path_for = functools.partial(_get_create_path_for, root) -+ path_for_type = lambda t: root / _get_path_for_type(t) - bot_docs_paths = [] - - for tlobject in tlobjects: - filename = create_path_for(tlobject) -- paths = {k: _get_relative_path(v, filename) -- for k, v in original_paths.items()} -- -- with DocsWriter(filename, type_to_path=path_for_type) as docs: -+ with DocsWriter(root, filename, path_for_type) as docs: - docs.write_head(title=tlobject.class_name, -- relative_css_path=paths['css'], -- default_css=original_paths['default_css']) -+ css_path=paths['css'], -+ default_css=paths['default_css']) - - # Create the menu (path to the current TLObject) - docs.set_menu_separator(paths['arrow']) -- _build_menu(docs, filename, output_dir, -- relative_main_index=paths['index_all']) -+ _build_menu(docs) - - # Create the page title - docs.write_title(tlobject.class_name) -@@ -333,9 +303,7 @@ - inner = tlobject.result - - docs.begin_table(column_count=1) -- docs.add_row(inner, link=path_for_type( -- inner, relative_to=filename -- )) -+ docs.add_row(inner, link=path_for_type(inner)) - docs.end_table() - - cs = type_to_constructors.get(inner, []) -@@ -349,7 +317,6 @@ - docs.begin_table(column_count=2) - for constructor in cs: - link = create_path_for(constructor) -- link = _get_relative_path(link, relative_to=filename) - docs.add_row(constructor.class_name, link=link) - docs.end_table() - -@@ -380,8 +347,8 @@ - docs.add_row('!' + friendly_type, align='center') - else: - docs.add_row( -- friendly_type, align='center', link= -- path_for_type(arg.type, relative_to=filename) -+ friendly_type, align='center', -+ link=path_for_type(arg.type) - ) - - # Add a description for this argument -@@ -441,18 +408,13 @@ - docs.add_script(relative_src=paths['search.js']) - docs.end_body() - -- temp = [] -- for item in bot_docs_paths: -- temp.append(os.path.sep.join(item.split(os.path.sep)[2:])) -- bot_docs_paths = temp -- - # Find all the available types (which are not the same as the constructors) - # Each type has a list of constructors associated to it, hence is a map - for t, cs in type_to_constructors.items(): - filename = path_for_type(t) -- out_dir = os.path.dirname(filename) -+ out_dir = filename.parent - if out_dir: -- os.makedirs(out_dir, exist_ok=True) -+ out_dir.mkdir(parents=True, exist_ok=True) - - # Since we don't have access to the full TLObject, split the type - if '.' in t: -@@ -460,17 +422,13 @@ - else: - namespace, name = None, t - -- paths = {k: _get_relative_path(v, out_dir, folder=True) -- for k, v in original_paths.items()} -- -- with DocsWriter(filename, type_to_path=path_for_type) as docs: -+ with DocsWriter(root, filename, path_for_type) as docs: - docs.write_head(title=snake_to_camel_case(name), -- relative_css_path=paths['css'], -- default_css=original_paths['default_css']) -+ css_path=paths['css'], -+ default_css=paths['default_css']) - - docs.set_menu_separator(paths['arrow']) -- _build_menu(docs, filename, output_dir, -- relative_main_index=paths['index_all']) -+ _build_menu(docs) - - # Main file title - docs.write_title(snake_to_camel_case(name)) -@@ -489,7 +447,6 @@ - for constructor in cs: - # Constructor full name - link = create_path_for(constructor) -- link = _get_relative_path(link, relative_to=filename) - docs.add_row(constructor.class_name, link=link) - docs.end_table() - -@@ -509,7 +466,6 @@ - docs.begin_table(2) - for func in functions: - link = create_path_for(func) -- link = _get_relative_path(link, relative_to=filename) - docs.add_row(func.class_name, link=link) - docs.end_table() - -@@ -534,7 +490,6 @@ - docs.begin_table(2) - for ot in other_methods: - link = create_path_for(ot) -- link = _get_relative_path(link, relative_to=filename) - docs.add_row(ot.class_name, link=link) - docs.end_table() - -@@ -560,7 +515,6 @@ - docs.begin_table(2) - for ot in other_types: - link = create_path_for(ot) -- link = _get_relative_path(link, relative_to=filename) - docs.add_row(ot.class_name, link=link) - docs.end_table() - docs.end_body() -@@ -570,11 +524,10 @@ - # information that we have available, simply a file listing all the others - # accessible by clicking on their title - for folder in ['types', 'methods', 'constructors']: -- _generate_index(os.path.join(output_dir, folder), original_paths, -- output_dir) -+ _generate_index(root, root / folder, paths) - -- _generate_index(os.path.join(output_dir, 'methods'), original_paths, -- output_dir, True, bot_docs_paths) -+ _generate_index(root, root / 'methods', paths, True, -+ bot_docs_paths) - - # Write the final core index, the main index for the rest of files - types = set() -@@ -596,9 +549,8 @@ - methods = sorted(methods, key=lambda m: m.name) - cs = sorted(cs, key=lambda c: c.name) - -- shutil.copy(os.path.join(input_res, '404.html'), original_paths['404']) -- _copy_replace(os.path.join(input_res, 'core.html'), -- original_paths['index_all'], { -+ shutil.copy(str(input_res / '404.html'), str(paths['404'])) -+ _copy_replace(input_res / 'core.html', paths['index_all'], { - '{type_count}': len(types), - '{method_count}': len(methods), - '{constructor_count}': len(tlobjects) - len(methods), -@@ -624,17 +576,15 @@ - type_names = fmt(types, formatter=lambda x: x) - - # Local URLs shouldn't rely on the output's root, so set empty root -- create_path_for = functools.partial(_get_create_path_for, '', make=False) -- path_for_type = functools.partial(_get_path_for_type, '') -+ create_path_for = functools.partial( -+ _get_create_path_for, Path(), make=False) -+ - request_urls = fmt(methods, create_path_for) -- type_urls = fmt(types, path_for_type) -+ type_urls = fmt(types, _get_path_for_type) - constructor_urls = fmt(cs, create_path_for) - -- os.makedirs(os.path.abspath(os.path.join( -- original_paths['search.js'], os.path.pardir -- )), exist_ok=True) -- _copy_replace(os.path.join(input_res, 'js', 'search.js'), -- original_paths['search.js'], { -+ paths['search.js'].parent.mkdir(parents=True, exist_ok=True) -+ _copy_replace(input_res / 'js/search.js', paths['search.js'], { - '{request_names}': request_names, - '{type_names}': type_names, - '{constructor_names}': constructor_names, -@@ -649,11 +599,11 @@ - ('img', ['arrow.svg'])]: -- dirpath = os.path.join(out_dir, dirname) -- os.makedirs(dirpath, exist_ok=True) -+ dirpath = out_dir / dirname -+ dirpath.mkdir(parents=True, exist_ok=True) - for file in files: -- shutil.copy(os.path.join(res_dir, dirname, file), dirpath) -+ shutil.copy(str(res_dir / dirname / file), str(dirpath)) - - - def generate_docs(tlobjects, methods, layer, input_res, output_dir): -- os.makedirs(output_dir, exist_ok=True) -- _write_html_pages(tlobjects, methods, layer, input_res, output_dir) -+ output_dir.mkdir(parents=True, exist_ok=True) -+ _write_html_pages(output_dir, tlobjects, methods, layer, input_res) - _copy_resources(input_res, output_dir) ---- a/telethon_generator/generators/tlobject.py -+++ b/telethon_generator/generators/tlobject.py -@@ -48,9 +48,8 @@ - def _write_modules( - out_dir, depth, kind, namespace_tlobjects, type_constructors): - # namespace_tlobjects: {'namespace', [TLObject]} -- os.makedirs(out_dir, exist_ok=True) -+ out_dir.mkdir(parents=True, exist_ok=True) - for ns, tlobjects in namespace_tlobjects.items(): -- file = os.path.join(out_dir, '{}.py'.format(ns or '__init__')) -- with open(file, 'w', encoding='utf-8') as f,\ -- SourceBuilder(f) as builder: -+ file = out_dir / '{}.py'.format(ns or '__init__') -+ with file.open('w') as f, SourceBuilder(f) as builder: - builder.writeln(AUTO_GEN_NOTICE) - - builder.writeln('from {}.tl.tlobject import TLObject', '.' * depth) -@@ -635,11 +634,10 @@ - - - def _write_patched(out_dir, namespace_tlobjects): -- os.makedirs(out_dir, exist_ok=True) -+ out_dir.mkdir(parents=True, exist_ok=True) - for ns, tlobjects in namespace_tlobjects.items(): -- file = os.path.join(out_dir, '{}.py'.format(ns or '__init__')) -- with open(file, 'w', encoding='utf-8') as f,\ -- SourceBuilder(f) as builder: -+ file = out_dir / '{}.py'.format(ns or '__init__') -+ with file.open('w') as f, SourceBuilder(f) as builder: - builder.writeln(AUTO_GEN_NOTICE) - - builder.writeln('import struct') -@@ -715,26 +713,24 @@ - if tlobject.fullname in PATCHED_TYPES: - namespace_patched[tlobject.namespace].append(tlobject) - -- get_file = functools.partial(os.path.join, output_dir) -- _write_modules(get_file('functions'), import_depth, 'TLRequest', -+ _write_modules(output_dir / 'functions', import_depth, 'TLRequest', - namespace_functions, type_constructors) -- _write_modules(get_file('types'), import_depth, 'TLObject', -+ _write_modules(output_dir / 'types', import_depth, 'TLObject', - namespace_types, type_constructors) -- _write_patched(get_file('patched'), namespace_patched) -+ _write_patched(output_dir / 'patched', namespace_patched) - -- filename = os.path.join(get_file('alltlobjects.py')) -- with open(filename, 'w', encoding='utf-8') as file: -+ filename = output_dir / 'alltlobjects.py' -+ with filename.open('w') as file: - with SourceBuilder(file) as builder: - _write_all_tlobjects(tlobjects, layer, builder) - - - def clean_tlobjects(output_dir): -- get_file = functools.partial(os.path.join, output_dir) - for d in ('functions', 'types'): -- d = get_file(d) -- if os.path.isdir(d): -- shutil.rmtree(d) -+ d = output_dir / d -+ if d.is_dir(): -+ shutil.rmtree(str(d)) - -- tl = get_file('alltlobjects.py') -- if os.path.isfile(tl): -- os.remove(tl) -+ tl = output_dir / 'alltlobjects.py' -+ if tl.is_file(): -+ tl.unlink() ---- a/telethon_generator/parsers/errors.py -+++ b/telethon_generator/parsers/errors.py -@@ -57,7 +57,7 @@ - Parses the input CSV file with columns (name, error codes, description) - and yields `Error` instances as a result. - """ -- with open(csv_file, newline='') as f: -+ with csv_file.open(newline='') as f: - f = csv.reader(f) - next(f, None) # header - for line, (name, codes, description) in enumerate(f, start=2): ---- a/telethon_generator/parsers/methods.py -+++ b/telethon_generator/parsers/methods.py -@@ -30,7 +30,7 @@ - Parses the input CSV file with columns (method, usability, errors) - and yields `MethodInfo` instances as a result. - """ -- with open(csv_file, newline='') as f: -+ with csv_file.open(newline='') as f: - f = csv.reader(f) - next(f, None) # header - for line, (method, usability, errors) in enumerate(f, start=2): ---- a/telethon_generator/parsers/tlobject/parser.py -+++ b/telethon_generator/parsers/tlobject/parser.py -@@ -86,7 +86,7 @@ - obj_all = [] - obj_by_name = {} - obj_by_type = collections.defaultdict(list) -- with open(file_path, 'r', encoding='utf-8') as file: -+ with file_path.open() as file: - is_function = False - for line in file: - comment_index = line.find('//') From aac4d03a705c36d804d76aa4151c4095863d90ca Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 25 Oct 2020 10:26:06 +0100 Subject: [PATCH 22/29] Cleanup .gitignore to contain only what's needed --- .gitignore | 105 +++++------------------------------------------------ 1 file changed, 10 insertions(+), 95 deletions(-) diff --git a/.gitignore b/.gitignore index e39a8281..a9f7111a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,3 @@ -# Docs -/_build/ -/docs/ - # Generated code /telethon/tl/functions/ /telethon/tl/types/ @@ -11,98 +7,17 @@ # User session *.session -usermedia/ +/usermedia/ -# Quick tests should live in this file -example.py - -# Byte-compiled / optimized / DLL files +# Builds and testing __pycache__/ -*.py[cod] -*$py.class +/dist/ +/*.egg-info/ +/readthedocs/_build/ +/.tox/ -# C extensions -*.so +# API reference docs +/docs/ -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*,cover -.hypothesis/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -/docs/_build/ - -# PyBuilder -target/ - -# IPython Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# dotenv -.env - -# virtualenv -.venv/ -venv/ -ENV/ - -# Spyder project settings -.spyderproject - -# Rope project settings -.ropeproject +# File used to manually test new changes, contains sensitive data +/example.py From e7f174cdc8fc41063b257baa7f70638415642d98 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 25 Oct 2020 10:33:36 +0100 Subject: [PATCH 23/29] Fix search with offset_date causing infinite recursion Bug introduced by 668dcd5. Closes #1606. --- telethon/client/messages.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telethon/client/messages.py b/telethon/client/messages.py index 28202a62..055f566d 100644 --- a/telethon/client/messages.py +++ b/telethon/client/messages.py @@ -133,7 +133,8 @@ class _MessagesIter(RequestIter): # # Even better, using `filter` and `from_id` seems to always # trigger `RPC_CALL_FAIL` which is "internal issues"... - if filter and offset_date and not search and not offset_id: + if not isinstance(filter, types.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 From 4ce2c0017a9c3af60abad2de769c24cc5717af1d Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 25 Oct 2020 10:49:38 +0100 Subject: [PATCH 24/29] Somewhat improve packaging situation (#1605) --- .gitignore | 1 + MANIFEST.in | 4 ---- setup.py | 14 +++++++++++--- telethon/version.py | 2 +- 4 files changed, 13 insertions(+), 8 deletions(-) delete mode 100644 MANIFEST.in diff --git a/.gitignore b/.gitignore index a9f7111a..dbfd60b5 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ # Builds and testing __pycache__/ /dist/ +/build/ /*.egg-info/ /readthedocs/_build/ /.tox/ diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 0eed5bd4..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,4 +0,0 @@ -include LICENSE -include README.rst - -recursive-include telethon * diff --git a/setup.py b/setup.py index 3446369d..676eda28 100755 --- a/setup.py +++ b/setup.py @@ -165,8 +165,14 @@ def main(argv): print('Packaging for PyPi aborted, importing the module failed.') return - for x in ('build', 'dist', 'Telethon.egg-info'): + remove_dirs = ['__pycache__', 'build', 'dist', 'Telethon.egg-info'] + for root, _dirs, _files in os.walk(LIBRARY_DIR, topdown=False): + # setuptools is including __pycache__ for some reason (#1605) + if root.endswith('/__pycache__'): + remove_dirs.append(root) + for x in remove_dirs: shutil.rmtree(x, ignore_errors=True) + run('python3 setup.py sdist', shell=True) run('python3 setup.py bdist_wheel', shell=True) run('twine upload dist/*', shell=True) @@ -218,11 +224,13 @@ def main(argv): 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6' + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', ], keywords='telegram api chat client library messaging mtproto', packages=find_packages(exclude=[ - 'telethon_*', 'run_tests.py', 'try_telethon.py' + 'telethon_*', 'tests' ]), install_requires=['pyaes', 'rsa'], extras_require={ diff --git a/telethon/version.py b/telethon/version.py index 9594a416..f6050a6c 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.17.4' +__version__ = '1.17.5' From 353b88ea5ab9c7edd2cbc1a328bc9c9914a26da0 Mon Sep 17 00:00:00 2001 From: Xiretza Date: Tue, 27 Oct 2020 12:31:31 +0100 Subject: [PATCH 25/29] Actually exclude tests from setup.py installation (#1612) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 676eda28..a498980a 100755 --- a/setup.py +++ b/setup.py @@ -230,7 +230,7 @@ def main(argv): ], keywords='telegram api chat client library messaging mtproto', packages=find_packages(exclude=[ - 'telethon_*', 'tests' + 'telethon_*', 'tests*' ]), install_requires=['pyaes', 'rsa'], extras_require={ From d83c154f8d6c83c5bfcb5af7b6181b75aa744f24 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Fri, 30 Oct 2020 20:06:31 +0100 Subject: [PATCH 26/29] Partial update to layer 120 --- telethon/client/messages.py | 2 +- telethon/events/chataction.py | 5 +++-- telethon/tl/custom/message.py | 8 +++++-- telethon_generator/data/api.tl | 38 +++++++++++++++++++--------------- 4 files changed, 31 insertions(+), 22 deletions(-) diff --git a/telethon/client/messages.py b/telethon/client/messages.py index 055f566d..1951942b 100644 --- a/telethon/client/messages.py +++ b/telethon/client/messages.py @@ -1247,7 +1247,7 @@ class MessageMethods: notify: bool = False ): """ - Pins or unpins a message in a chat. + Pins a message in a chat. The default behaviour is to *not* notify members, unlike the official applications. diff --git a/telethon/events/chataction.py b/telethon/events/chataction.py index e1b541f0..bde7d66f 100644 --- a/telethon/events/chataction.py +++ b/telethon/events/chataction.py @@ -31,14 +31,15 @@ class ChatAction(EventBuilder): """ @classmethod def build(cls, update, others=None, self_id=None): - if isinstance(update, types.UpdateChannelPinnedMessage) and update.id == 0: + raise RuntimeError('FIXME: handle new pinned updates') + if isinstance(update, types.UpdatePinnedChannelMessages) and update.id == 0: # Telegram does not always send # UpdateChannelPinnedMessage for new pins # but always for unpin, with update.id = 0 return cls.Event(types.PeerChannel(update.channel_id), unpin=True) - elif isinstance(update, types.UpdateChatPinnedMessage) and update.id == 0: + elif isinstance(update, types.UpdatePinnedMessages) and update.id == 0: return cls.Event(types.PeerChat(update.chat_id), unpin=True) diff --git a/telethon/tl/custom/message.py b/telethon/tl/custom/message.py index e0b2d24c..ac11dc97 100644 --- a/telethon/tl/custom/message.py +++ b/telethon/tl/custom/message.py @@ -61,6 +61,9 @@ class Message(ChatGetter, SenderGetter, TLObject, abc.ABC): 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`. @@ -166,8 +169,8 @@ class Message(ChatGetter, SenderGetter, TLObject, abc.ABC): fwd_from=None, via_bot_id=None, media=None, reply_markup=None, entities=None, views=None, edit_date=None, post_author=None, grouped_id=None, from_scheduled=None, legacy=None, - edit_hide=None, restriction_reason=None, forwards=None, - replies=None, + edit_hide=None, pinned=None, restriction_reason=None, + forwards=None, replies=None, # For MessageAction (mandatory) action=None): @@ -195,6 +198,7 @@ class Message(ChatGetter, SenderGetter, TLObject, abc.ABC): 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 diff --git a/telethon_generator/data/api.tl b/telethon_generator/data/api.tl index ce2e3b0b..06f21cc2 100644 --- a/telethon_generator/data/api.tl +++ b/telethon_generator/data/api.tl @@ -69,7 +69,7 @@ inputMediaPhotoExternal#e5bbfe1a flags:# url:string ttl_seconds:flags.0?int = In inputMediaDocumentExternal#fb52dc99 flags:# url:string ttl_seconds:flags.0?int = InputMedia; inputMediaGame#d33f43f3 id:InputGame = InputMedia; inputMediaInvoice#f4e096c3 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:string = InputMedia; -inputMediaGeoLive#ce4e82fd flags:# stopped:flags.0?true geo_point:InputGeoPoint period:flags.1?int = InputMedia; +inputMediaGeoLive#971fa843 flags:# stopped:flags.0?true geo_point:InputGeoPoint heading:flags.2?int period:flags.1?int proximity_notification_radius:flags.3?int = InputMedia; inputMediaPoll#f94e5f1 flags:# poll:Poll correct_answers:flags.0?Vector solution:flags.1?string solution_entities:flags.1?Vector = InputMedia; inputMediaDice#e66fbf7b emoticon:string = InputMedia; @@ -78,7 +78,7 @@ inputChatUploadedPhoto#c642724e flags:# file:flags.0?InputFile video:flags.1?Inp inputChatPhoto#8953ad37 id:InputPhoto = InputChatPhoto; inputGeoPointEmpty#e4c123d6 = InputGeoPoint; -inputGeoPoint#f3b7acc9 lat:double long:double = InputGeoPoint; +inputGeoPoint#48222faf flags:# lat:double long:double accuracy_radius:flags.0?int = InputGeoPoint; inputPhotoEmpty#1cd7bf0d = InputPhoto; inputPhoto#3bb3b94a id:long access_hash:long file_reference:bytes = InputPhoto; @@ -141,7 +141,7 @@ chatPhotoEmpty#37c1011c = ChatPhoto; chatPhoto#d20b9f3c flags:# has_video:flags.0?true photo_small:FileLocation photo_big:FileLocation dc_id:int = ChatPhoto; messageEmpty#83e5de54 id:int = Message; -message#58ae39c9 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 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 = Message; +message#58ae39c9 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 = Message; messageService#286fa604 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 = Message; messageMediaEmpty#3ded6320 = MessageMedia; @@ -154,7 +154,7 @@ messageMediaWebPage#a32dd600 webpage:WebPage = MessageMedia; messageMediaVenue#2ec0533f geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string = MessageMedia; messageMediaGame#fdb19008 game:Game = MessageMedia; messageMediaInvoice#84551347 flags:# shipping_address_requested:flags.1?true test:flags.3?true title:string description:string photo:flags.0?WebDocument receipt_msg_id:flags.2?int currency:string total_amount:long start_param:string = MessageMedia; -messageMediaGeoLive#7c3c2609 geo:GeoPoint period:int = MessageMedia; +messageMediaGeoLive#b940c666 flags:# geo:GeoPoint heading:flags.0?int period:int proximity_notification_radius:flags.1?int = MessageMedia; messageMediaPoll#4bd6e798 poll:Poll results:PollResults = MessageMedia; messageMediaDice#3f7ee58b value:int emoticon:string = MessageMedia; @@ -181,6 +181,7 @@ messageActionBotAllowed#abe9affe domain:string = MessageAction; messageActionSecureValuesSentMe#1b287353 values:Vector credentials:SecureCredentialsEncrypted = MessageAction; messageActionSecureValuesSent#d95c6154 types:Vector = MessageAction; messageActionContactSignUp#f3f25f76 = MessageAction; +messageActionGeoProximityReached#98e0d697 from_id:Peer to_id:Peer distance:int = 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; @@ -195,7 +196,7 @@ photoStrippedSize#e0b0bc2e type:string bytes:bytes = PhotoSize; photoSizeProgressive#5aa86a51 type:string location:FileLocation w:int h:int sizes:Vector = PhotoSize; geoPointEmpty#1117dd5f = GeoPoint; -geoPoint#296f104 long:double lat:double access_hash:long = GeoPoint; +geoPoint#b2a2f663 flags:# long:double lat:double access_hash:long accuracy_radius:flags.0?int = GeoPoint; auth.sentCode#5e002502 flags:# type:auth.SentCodeType phone_code_hash:string next_type:flags.1?auth.CodeType timeout:flags.2?int = auth.SentCode; @@ -247,8 +248,8 @@ messages.dialogsSlice#71e094f3 count:int dialogs:Vector messages:Vector< messages.dialogsNotModified#f0e3e596 count:int = messages.Dialogs; messages.messages#8c718e87 messages:Vector chats:Vector users:Vector = messages.Messages; -messages.messagesSlice#c8edce1e flags:# inexact:flags.1?true count:int next_rate:flags.0?int messages:Vector chats:Vector users:Vector = messages.Messages; -messages.channelMessages#99262e37 flags:# inexact:flags.1?true pts:int count:int messages:Vector chats:Vector users:Vector = messages.Messages; +messages.messagesSlice#3a54685e flags:# inexact:flags.1?true count:int next_rate:flags.0?int offset_id_offset:flags.2?int messages:Vector chats:Vector users:Vector = messages.Messages; +messages.channelMessages#64479808 flags:# inexact:flags.1?true pts:int count:int offset_id_offset:flags.2?int messages:Vector chats:Vector users:Vector = messages.Messages; messages.messagesNotModified#74535f21 count:int = messages.Messages; messages.chats#64ff9fd5 chats:Vector = messages.Chats; @@ -274,6 +275,7 @@ inputMessagesFilterRoundVideo#b549da53 = MessagesFilter; inputMessagesFilterMyMentions#c1f8e69a = MessagesFilter; inputMessagesFilterGeo#e7026d0d = MessagesFilter; inputMessagesFilterContacts#e062db83 = MessagesFilter; +inputMessagesFilterPinned#1bb00451 = MessagesFilter; updateNewMessage#1f2b0afd message:Message pts:int pts_count:int = Update; updateMessageID#4e90bfd6 id:int random_id:long = Update; @@ -313,7 +315,6 @@ updateSavedGifs#9375341e = Update; updateBotInlineQuery#54826690 flags:# query_id:long user_id:int query:string geo:flags.0?GeoPoint offset:string = Update; updateBotInlineSend#e48f964 flags:# user_id:int query:string geo:flags.0?GeoPoint id:string msg_id:flags.1?InputBotInlineMessageID = Update; updateEditChannelMessage#1b3f4df7 message:Message pts:int pts_count:int = Update; -updateChannelPinnedMessage#98592475 channel_id:int id: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; 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; @@ -338,8 +339,6 @@ updateChannelReadMessagesContents#89893b45 channel_id:int messages:Vector = updateContactsReset#7084a7be = Update; updateChannelAvailableMessages#70db6837 channel_id:int available_min_id:int = Update; updateDialogUnreadMark#e16459c3 flags:# unread:flags.0?true peer:DialogPeer = Update; -updateUserPinnedMessage#4c43da18 user_id:int id:int = Update; -updateChatPinnedMessage#e10db349 chat_id:int id:int version:int = 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; updateFolderPeers#19360dc0 folder_peers:Vector pts:int pts_count:int = Update; @@ -361,6 +360,8 @@ updateReadChannelDiscussionInbox#1cc7de54 flags:# channel_id:int top_msg_id:int updateReadChannelDiscussionOutbox#4638a26c channel_id:int top_msg_id:int read_max_id:int = Update; updatePeerBlocked#246a4b22 peer_id:Peer blocked:Bool = Update; updateChannelUserTyping#ff2abe9f flags:# channel_id:int top_msg_id:flags.0?int user_id:int 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; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; @@ -607,6 +608,7 @@ channelParticipantSelf#a3289a6d user_id:int inviter_id:int date:int = ChannelPar 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#1c0facaf flags:# left:flags.0?true user_id:int kicked_by:int date:int banned_rights:ChatBannedRights = ChannelParticipant; +channelParticipantLeft#c3c6796b user_id:int = ChannelParticipant; channelParticipantsRecent#de3f3c79 = ChannelParticipantsFilter; channelParticipantsAdmins#b4608969 = ChannelParticipantsFilter; @@ -615,6 +617,7 @@ channelParticipantsBots#b0d1865b = ChannelParticipantsFilter; channelParticipantsBanned#1427a5e1 q:string = ChannelParticipantsFilter; channelParticipantsSearch#656ac4b q:string = ChannelParticipantsFilter; channelParticipantsContacts#bb6ae88d q:string = ChannelParticipantsFilter; +channelParticipantsMentions#e04b5ceb flags:# q:flags.0?string top_msg_id:flags.1?int = ChannelParticipantsFilter; channels.channelParticipants#f56ee2a8 count:int participants:Vector users:Vector = channels.ChannelParticipants; channels.channelParticipantsNotModified#f0173fe9 = channels.ChannelParticipants; @@ -628,7 +631,7 @@ messages.savedGifs#2e0709a5 hash:int 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; -inputBotInlineMessageMediaGeo#c1b15d65 flags:# geo_point:InputGeoPoint period:int reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; +inputBotInlineMessageMediaGeo#96929a85 flags:# geo_point:InputGeoPoint heading:flags.0?int period:flags.1?int proximity_notification_radius:flags.3?int reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageMediaVenue#417bbf11 flags:# geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageMediaContact#a6edbffd flags:# phone_number:string first_name:string last_name:string vcard:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageGame#4b425864 flags:# reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; @@ -640,7 +643,7 @@ inputBotInlineResultGame#4fa417f2 id:string short_name:string send_message:Input botInlineMessageMediaAuto#764cf810 flags:# message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineMessageText#8c7f65e2 flags:# no_webpage:flags.0?true message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = BotInlineMessage; -botInlineMessageMediaGeo#b722de65 flags:# geo:GeoPoint period:int reply_markup:flags.2?ReplyMarkup = BotInlineMessage; +botInlineMessageMediaGeo#51846fd flags:# geo:GeoPoint heading:flags.0?int period:flags.1?int proximity_notification_radius:flags.3?int reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineMessageMediaVenue#8a86659c flags:# geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineMessageMediaContact#18d1cdc2 flags:# phone_number:string first_name:string last_name:string vcard:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage; @@ -1164,8 +1167,6 @@ messageViews#455b853d flags:# views:flags.0?int forwards:flags.1?int replies:fla messages.messageViews#b6c4f543 views:Vector chats:Vector users:Vector = messages.MessageViews; -stats.messageStats#8999f295 views_graph:StatsGraph = stats.MessageStats; - 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; messageReplyHeader#a6d57763 flags:# reply_to_msg_id:int reply_to_peer_id:flags.0?Peer reply_to_top_id:flags.1?int = MessageReplyHeader; @@ -1174,6 +1175,8 @@ messageReplies#4128faac flags:# comments:flags.0?true replies:int replies_pts:in peerBlocked#e8fd8014 peer_id:Peer date:int = PeerBlocked; +stats.messageStats#8999f295 views_graph:StatsGraph = stats.MessageStats; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -1299,7 +1302,7 @@ contacts.blockFromReplies#29a8962c flags:# delete_message:flags.0?true delete_hi 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#4e17810b flags:# peer:InputPeer q:string from_id:flags.0?InputUser 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.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.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; @@ -1393,7 +1396,7 @@ messages.getSplitRanges#1cff7e08 = Vector; messages.markDialogUnread#c286d98f flags:# unread:flags.0?true peer:InputDialogPeer = Bool; messages.getDialogUnreadMarks#22e24e22 = Vector; messages.clearAllDrafts#7e58ee9c = Bool; -messages.updatePinnedMessage#d2aaf7ec flags:# silent:flags.0?true peer:InputPeer id:int = Updates; +messages.updatePinnedMessage#d2aaf7ec flags:# silent:flags.0?true unpin:flags.1?true pm_oneside:flags.2?true peer:InputPeer id:int = Updates; 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; @@ -1422,6 +1425,7 @@ messages.getOldFeaturedStickers#5fe7025b offset:int limit:int hash:int = message 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.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; updates.getState#edd4882a = updates.State; updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference; @@ -1543,4 +1547,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 119 +// LAYER 120 From 9e3cb8180bbf398629a813b5c6d144c95bc128c8 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 31 Oct 2020 11:21:38 +0100 Subject: [PATCH 27/29] Update ChatAction to handle new pin updates --- telethon/events/chataction.py | 65 +++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/telethon/events/chataction.py b/telethon/events/chataction.py index bde7d66f..cf787856 100644 --- a/telethon/events/chataction.py +++ b/telethon/events/chataction.py @@ -31,17 +31,15 @@ class ChatAction(EventBuilder): """ @classmethod def build(cls, update, others=None, self_id=None): - raise RuntimeError('FIXME: handle new pinned updates') - if isinstance(update, types.UpdatePinnedChannelMessages) and update.id == 0: - # Telegram does not always send - # UpdateChannelPinnedMessage for new pins - # but always for unpin, with update.id = 0 + if isinstance(update, types.UpdatePinnedChannelMessages): return cls.Event(types.PeerChannel(update.channel_id), - unpin=True) + pin_ids=update.messages, + pin=update.pinned) - elif isinstance(update, types.UpdatePinnedMessages) and update.id == 0: - return cls.Event(types.PeerChat(update.chat_id), - unpin=True) + elif isinstance(update, types.UpdatePinnedMessages): + return cls.Event(update.peer, + pin_ids=update.messages, + pin=update.pinned) elif isinstance(update, types.UpdateChatParticipantAdd): return cls.Event(types.PeerChat(update.chat_id), @@ -109,12 +107,8 @@ class ChatAction(EventBuilder): return cls.Event(msg, users=msg.from_id, new_photo=True) - elif isinstance(action, types.MessageActionPinMessage) and msg.reply_to: - # Seems to not be reliable on unpins, but when pinning - # we prefer this because we know who caused it. - return cls.Event(msg, - users=msg.from_id, - new_pin=msg.reply_to.reply_to_msg_id) + # Handled by specific updates + # elif isinstance(action, types.MessageActionPinMessage) and msg.reply_to: class Event(EventCommon): """ @@ -154,19 +148,22 @@ class ChatAction(EventBuilder): unpin (`bool`): `True` if the existing pin gets unpinned. """ - def __init__(self, where, new_pin=None, new_photo=None, + def __init__(self, where, new_photo=None, added_by=None, kicked_by=None, created=None, - users=None, new_title=None, unpin=None): + users=None, new_title=None, pin_ids=None, pin=None): if isinstance(where, types.MessageService): self.action_message = where where = where.peer_id else: self.action_message = None - super().__init__(chat_peer=where, msg_id=new_pin) + # 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 = isinstance(new_pin, int) - self._pinned_message = new_pin + 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 = \ @@ -203,7 +200,7 @@ class ChatAction(EventBuilder): self._users = None self._input_users = None self.new_title = new_title - self.unpin = unpin + self.unpin = not pin def _set_client(self, client): super()._set_client(client) @@ -257,16 +254,26 @@ class ChatAction(EventBuilder): If ``new_pin`` is `True`, this returns the `Message ` object that was pinned. """ - if self._pinned_message == 0: - return None + if self._pinned_messages is None: + await self.get_pinned_messages() - if isinstance(self._pinned_message, int)\ - and await self.get_input_chat(): - self._pinned_message = await self._client.get_messages( - self._input_chat, ids=self._pinned_message) + if self._pinned_messages: + return self._pinned_messages[0] - if isinstance(self._pinned_message, types.Message): - return self._pinned_message + 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 + + chat = await self.get_input_chat() + if chat: + self._pinned_messages = await self._client.get_messages( + self._input_chat, ids=self._pin_ids) + + return self._pinned_messages @property def added_by(self): From 935ee2242db1fb68146a8a1d3f008016efeb82ff Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 31 Oct 2020 11:31:09 +0100 Subject: [PATCH 28/29] Add method to unpin messages --- telethon/client/messages.py | 47 +++++++++++++++++++++++++++++++---- telethon/tl/custom/message.py | 10 ++++++++ 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/telethon/client/messages.py b/telethon/client/messages.py index 1951942b..628d2a1b 100644 --- a/telethon/client/messages.py +++ b/telethon/client/messages.py @@ -1260,7 +1260,7 @@ class MessageMethods: message (`int` | `Message `): The message or the message ID to pin. If it's - `None`, the message will be unpinned instead. + `None`, all messages will be unpinned instead. notify (`bool`, optional): Whether the pin should notify people or not. @@ -1272,18 +1272,55 @@ 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) + + 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): 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 + silent=not notify, + unpin=unpin, ) result = await self(request) - # Unpinning does not produce a service message, and technically - # users can pass negative IDs which seem to behave as unpinning too. - if message <= 0: + # Unpinning does not produce a service message + if unpin: return # Pinning in User chats (just with yourself really) does not produce a service message diff --git a/telethon/tl/custom/message.py b/telethon/tl/custom/message.py index ac11dc97..019ab3f8 100644 --- a/telethon/tl/custom/message.py +++ b/telethon/tl/custom/message.py @@ -1011,6 +1011,16 @@ class Message(ChatGetter, SenderGetter, TLObject, abc.ABC): return await self._client.pin_message( await self.get_input_chat(), self.id, notify=notify) + async def unpin(self): + """ + Unpins the message. Shorthand for + `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) + # endregion Public Methods # region Private Methods From 64d751a39717ccf94f786867648c3ce11e652631 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 31 Oct 2020 11:41:37 +0100 Subject: [PATCH 29/29] messages.search from_user may now be a non-User --- telethon/client/messages.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/telethon/client/messages.py b/telethon/client/messages.py index 628d2a1b..7dc25e64 100644 --- a/telethon/client/messages.py +++ b/telethon/client/messages.py @@ -58,11 +58,6 @@ class _MessagesIter(RequestIter): if from_user: from_user = await self.client.get_input_entity(from_user) - ty = helpers._entity_type(from_user) - if ty != helpers._EntityType.USER: - from_user = None # Ignore from_user unless it's a user - - if from_user: self.from_id = await self.client.get_peer_id(from_user) else: self.from_id = None @@ -406,8 +401,7 @@ class MessageMethods: containing photos. from_user (`entity`): - Only messages from this user will be returned. - This parameter will be ignored if it is not a user. + Only messages from this entity will be returned. wait_time (`int`): Wait time (in seconds) between different