diff --git a/readthedocs/conf.py b/readthedocs/conf.py index 35dadb24..2821e069 100644 --- a/readthedocs/conf.py +++ b/readthedocs/conf.py @@ -42,6 +42,9 @@ extensions = [ 'custom_roles' ] +# Change the default role so we can avoid prefixing everything with :obj: +default_role = "py:obj" + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -157,7 +160,7 @@ latex_elements = { # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'Telethon.tex', 'Telethon Documentation', - 'Jeff', 'manual'), + author, 'manual'), ] diff --git a/readthedocs/extra/basic/creating-a-client.rst b/readthedocs/extra/basic/creating-a-client.rst index 03e9f932..d485d9f9 100644 --- a/readthedocs/extra/basic/creating-a-client.rst +++ b/readthedocs/extra/basic/creating-a-client.rst @@ -163,36 +163,44 @@ The mentioned ``.start()`` method will handle this for you as well, but you must set the ``password=`` parameter beforehand (it won't be asked). If you don't have 2FA enabled, but you would like to do so through the library, -take as example the following code snippet: +use ``client.edit_2fa()``. +Be sure to know what you're doing when using this function and +you won't run into any problems. +Take note that if you want to set only the email/hint and leave +the current password unchanged, you need to "redo" the 2fa. + +See the examples below: .. code-block:: python - import os - from hashlib import sha256 - from telethon.tl.functions import account - from telethon.tl.types.account import PasswordInputSettings - - new_salt = client(account.GetPasswordRequest()).new_salt - salt = new_salt + os.urandom(8) # new random salt - - pw = 'secret'.encode('utf-8') # type your new password here - hint = 'hint' - - pw_salted = salt + pw + salt - pw_hash = sha256(pw_salted).digest() - - result = await client(account.UpdatePasswordSettingsRequest( - current_password_hash=salt, - new_settings=PasswordInputSettings( - new_salt=salt, - new_password_hash=pw_hash, - hint=hint - ) - )) - -Thanks to `Issue 259 `_ -for the tip! - + from telethon.errors import EmailUnconfirmedError + + # Sets 2FA password for first time: + await client.edit_2fa(new_password='supersecurepassword') + + # Changes password: + await client.edit_2fa(current_password='supersecurepassword', + new_password='changedmymind') + + # Clears current password (i.e. removes 2FA): + await client.edit_2fa(current_password='changedmymind', new_password=None) + + # Sets new password with recovery email: + try: + await client.edit_2fa(new_password='memes and dreams', + email='JohnSmith@example.com') + # Raises error (you need to check your email to complete 2FA setup.) + except EmailUnconfirmedError: + # You can put email checking code here if desired. + pass + + # Also take note that unless you remove 2FA or explicitly + # give email parameter again it will keep the last used setting + + # Set hint after already setting password: + await client.edit_2fa(current_password='memes and dreams', + new_password='memes and dreams', + hint='It keeps you alive') __ https://github.com/Anorov/PySocks#installation __ https://github.com/Anorov/PySocks#usage-1 diff --git a/readthedocs/extra/basic/entities.rst b/readthedocs/extra/basic/entities.rst index 30ccc90c..6bb436b5 100644 --- a/readthedocs/extra/basic/entities.rst +++ b/readthedocs/extra/basic/entities.rst @@ -32,7 +32,7 @@ you're able to just do this: # Dialogs are the "conversations you have open". # This method returns a list of Dialog, which # has the .entity attribute and other information. - dialogs = await client.get_dialogs(limit=200) + dialogs = await client.get_dialogs() # All of these work and do the same. lonami = await client.get_entity('lonami') @@ -44,27 +44,17 @@ you're able to just do this: contact = await client.get_entity('+34xxxxxxxxx') friend = await client.get_entity(friend_id) - # Using Peer/InputPeer (note that the API may return these) - # users, chats and channels may all have the same ID, so it's - # necessary to wrap (at least) chat and channels inside Peer. - # - # NOTICE how the IDs *must* be wrapped inside a Peer() so the - # library knows their type. + # Getting entities through their ID (User, Chat or Channel) + entity = await client.get_entity(some_id) + + # You can be more explicit about the type for said ID by wrapping + # it inside a Peer instance. This is recommended but not necessary. from telethon.tl.types import PeerUser, PeerChat, PeerChannel my_user = await client.get_entity(PeerUser(some_id)) my_chat = await client.get_entity(PeerChat(some_id)) my_channel = await client.get_entity(PeerChannel(some_id)) -.. warning:: - - As it has been mentioned already, getting the entity of a channel - through e.g. ``client.get_entity(channel id)`` will **not** work. - You would use ``client.get_entity(types.PeerChannel(channel id))``. - Remember that supergroups are channels and normal groups are chats. - This is a common mistake! - - All methods in the :ref:`telegram-client` call ``.get_input_entity()`` prior to sending the requst to save you from the hassle of doing so manually. That way, convenience calls such as ``client.send_message('lonami', 'hi!')`` diff --git a/readthedocs/extra/changelog.rst b/readthedocs/extra/changelog.rst index 612547af..e7973363 100644 --- a/readthedocs/extra/changelog.rst +++ b/readthedocs/extra/changelog.rst @@ -14,6 +14,36 @@ it can take advantage of new goodies! .. contents:: List of All Versions +Several bug fixes (v0.18.2) +=========================== + +*Published at 2018/03/27* + +Just a few bug fixes before they become too many. + +Additions +~~~~~~~~~ + +- Getting an entity by its positive ID should be enough, regardless of their + type (whether it's an ``User``, a ``Chat`` or a ``Channel``). Although + wrapping them inside a ``Peer`` is still recommended, it's not necessary. +- New ``client.edit_2fa`` function to change your Two Factor Authentication + settings. +- ``.stringify()`` and string representation for custom ``Dialog/Draft``. + +Bug fixes +~~~~~~~~~ + +- Some bug regarding ``.get_input_peer``. +- ``events.ChatAction`` wasn't picking up all the pins. +- ``force_document=True`` was being ignored for albums. +- Now you're able to send ``Photo`` and ``Document`` as files. +- Wrong access to a member on chat forbidden error for ``.get_participants``. + An empty list is returned instead. +- ``me/self`` check for ``.get[_input]_entity`` has been moved up so if + someone has "me" or "self" as their name they won't be retrieved. + + Iterator methods (v0.18.1) ========================== diff --git a/readthedocs/extra/examples/chats-and-channels.rst b/readthedocs/extra/examples/chats-and-channels.rst index cdaed4cd..25f6b8a5 100644 --- a/readthedocs/extra/examples/chats-and-channels.rst +++ b/readthedocs/extra/examples/chats-and-channels.rst @@ -11,10 +11,9 @@ Working with Chats and Channels Joining a chat or channel ************************* -Note that `Chat`__\ s are normal groups, and `Channel`__\ s are a -special form of `Chat`__\ s, -which can also be super-groups if their ``megagroup`` member is -``True``. +Note that :tl:`Chat` are normal groups, and :tl:`Channel` are a +special form of ``Chat``, which can also be super-groups if +their ``megagroup`` member is ``True``. Joining a public channel @@ -101,6 +100,13 @@ __ https://lonamiwebs.github.io/Telethon/methods/messages/check_chat_invite.html Retrieving all chat members (channels too) ****************************************** +You can use +`client.get_participants ` +to retrieve the participants (click it to see the relevant parameters). +Most of the time you will just need ``client.get_participants(entity)``. + +This is what said method is doing behind the scenes as an example. + In order to get all the members from a mega-group or channel, you need to use `GetParticipantsRequest`__. As we can see it needs an `InputChannel`__, (passing the mega-group or channel you're going to @@ -134,9 +140,10 @@ a fixed limit: .. note:: - It is **not** possible to get more than 10,000 members from a - group. It's a hard limit impossed by Telegram and there is - nothing you can do about it. Refer to `issue 573`__ for more. + If you need more than 10,000 members from a group you should use the + mentioned ``client.get_participants(..., aggressive=True)``. It will + do some tricks behind the scenes to get as many entities as possible. + Refer to `issue 573`__ for more on this. Note that ``GetParticipantsRequest`` returns `ChannelParticipants`__, @@ -147,8 +154,8 @@ __ https://lonamiwebs.github.io/Telethon/methods/channels/get_participants.html __ https://lonamiwebs.github.io/Telethon/methods/channels/get_participants.html __ https://lonamiwebs.github.io/Telethon/types/channel_participants_filter.html __ https://lonamiwebs.github.io/Telethon/constructors/channel_participants_search.html -__ https://lonamiwebs.github.io/Telethon/constructors/channels/channel_participants.html __ https://github.com/LonamiWebs/Telethon/issues/573 +__ https://lonamiwebs.github.io/Telethon/constructors/channels/channel_participants.html Recent Actions diff --git a/readthedocs/extra/examples/working-with-messages.rst b/readthedocs/extra/examples/working-with-messages.rst index dbe1ed93..06f4d85c 100644 --- a/readthedocs/extra/examples/working-with-messages.rst +++ b/readthedocs/extra/examples/working-with-messages.rst @@ -11,18 +11,27 @@ Working with messages Forwarding messages ******************* -Note that ForwardMessageRequest_ (note it's Message, singular) will *not* -work if channels are involved. This is because channel (and megagroups) IDs -are not unique, so you also need to know who the sender is (a parameter this -request doesn't have). - -Either way, you are encouraged to use ForwardMessagesRequest_ (note it's -Message*s*, plural) *always*, since it is more powerful, as follows: +This request is available as a friendly method through +`client.forward_messages `, +and can be used like shown below: .. code-block:: python + # If you only have the message IDs + await client.forward_messages( + entity, # to which entity you are forwarding the messages + message_ids, # the IDs of the messages (or message) to forward + from_entity # who sent the messages? + ) + + # If you have ``Message`` objects + await client.forward_messages( + entity, # to which entity you are forwarding the messages + messages # the messages (or message) to forward + ) + + # You can also do it manually if you prefer from telethon.tl.functions.messages import ForwardMessagesRequest - # note the s ^ messages = foo() # retrieve a few messages (or even one, in a list) from_entity = bar() @@ -119,7 +128,6 @@ send yourself the very first sticker you have: )) -.. _ForwardMessageRequest: https://lonamiwebs.github.io/Telethon/methods/messages/forward_message.html .. _ForwardMessagesRequest: https://lonamiwebs.github.io/Telethon/methods/messages/forward_messages.html .. _SearchRequest: https://lonamiwebs.github.io/Telethon/methods/messages/search.html .. _issues: https://github.com/LonamiWebs/Telethon/issues/215 diff --git a/telethon/errors/__init__.py b/telethon/errors/__init__.py index 9126aca3..d9875849 100644 --- a/telethon/errors/__init__.py +++ b/telethon/errors/__init__.py @@ -78,6 +78,9 @@ def rpc_message_to_error(code, message, report_method=None): if code == 404: return NotFoundError(message) + if code == 406: + return AuthKeyError(message) + if code == 500: return ServerError(message) diff --git a/telethon/errors/rpc_base_errors.py b/telethon/errors/rpc_base_errors.py index 467b256c..d2db5439 100644 --- a/telethon/errors/rpc_base_errors.py +++ b/telethon/errors/rpc_base_errors.py @@ -56,6 +56,19 @@ class NotFoundError(RPCError): self.message = message +class AuthKeyError(RPCError): + """ + Errors related to invalid authorization key, like + AUTH_KEY_DUPLICATED which can cause the connection to fail. + """ + code = 406 + message = 'AUTH_KEY' + + def __init__(self, message): + super().__init__(message) + self.message = message + + class FloodError(RPCError): """ The maximum allowed number of attempts to invoke the given method diff --git a/telethon/events/__init__.py b/telethon/events/__init__.py index 94b6f7c6..959e0585 100644 --- a/telethon/events/__init__.py +++ b/telethon/events/__init__.py @@ -46,11 +46,11 @@ class _EventBuilder(abc.ABC): The common event builder, with builtin support to filter per chat. Args: - chats (:obj:`entity`, optional): + chats (`entity`, optional): May be one or more entities (username/peer/etc.). By default, only matching chats will be handled. - blacklist_chats (:obj:`bool`, optional): + blacklist_chats (`bool`, optional): Whether to treat the chats as a blacklist instead of as a whitelist (default). This means that every chat will be handled *except* those specified in ``chats`` @@ -118,11 +118,15 @@ class _EventCommon(abc.ABC): try: if isinstance(chat, types.InputPeerChannel): result = await self._client( - functions.channels.GetMessagesRequest(chat, [msg_id]) + functions.channels.GetMessagesRequest(chat, [ + types.InputMessageID(msg_id) + ]) ) else: result = await self._client( - functions.messages.GetMessagesRequest([msg_id]) + functions.messages.GetMessagesRequest([ + types.InputMessageID(msg_id) + ]) ) except RPCError: return None, None @@ -228,15 +232,15 @@ class NewMessage(_EventBuilder): Represents a new message event builder. Args: - incoming (:obj:`bool`, optional): + incoming (`bool`, optional): If set to ``True``, only **incoming** messages will be handled. Mutually exclusive with ``outgoing`` (can only set one of either). - outgoing (:obj:`bool`, optional): + outgoing (`bool`, optional): If set to ``True``, only **outgoing** messages will be handled. Mutually exclusive with ``incoming`` (can only set one of either). - pattern (:obj:`str`, :obj:`callable`, :obj:`Pattern`, optional): + pattern (`str`, `callable`, `Pattern`, optional): If set, only messages matching this pattern will be handled. You can specify a regex-like string which will be matched against the message, a callable function that returns ``True`` @@ -300,7 +304,7 @@ class NewMessage(_EventBuilder): else: return - event._entities = update.entities + event._entities = update._entities return self._message_filter_event(event) def _message_filter_event(self, event): @@ -330,16 +334,16 @@ class NewMessage(_EventBuilder): message (:tl:`Message`): This is the original :tl:`Message` object. - is_private (:obj:`bool`): + is_private (`bool`): True if the message was sent as a private message. - is_group (:obj:`bool`): + is_group (`bool`): True if the message was sent on a group or megagroup. - is_channel (:obj:`bool`): + is_channel (`bool`): True if the message was sent on a megagroup or channel. - is_reply (:obj:`str`): + is_reply (`str`): Whether the message is a reply to some other or not. """ def __init__(self, message): @@ -501,11 +505,13 @@ class NewMessage(_EventBuilder): if self._reply_message is None: if isinstance(await self.input_chat, types.InputPeerChannel): r = await self._client(functions.channels.GetMessagesRequest( - await self.input_chat, [self.message.reply_to_msg_id] + await self.input_chat, [ + types.InputMessageID(self.message.reply_to_msg_id) + ] )) else: r = await self._client(functions.messages.GetMessagesRequest( - [self.message.reply_to_msg_id] + [types.InputMessageID(self.message.reply_to_msg_id)] )) if not isinstance(r, types.messages.MessagesNotModified): self._reply_message = r.messages[0] @@ -692,7 +698,7 @@ class ChatAction(_EventBuilder): else: return - event._entities = update.entities + event._entities = update._entities return self._filter_event(event) class Event(_EventCommon): @@ -700,35 +706,35 @@ class ChatAction(_EventBuilder): Represents the event of a new chat action. Members: - new_pin (:obj:`bool`): + new_pin (`bool`): ``True`` if there is a new pin. - new_photo (:obj:`bool`): + new_photo (`bool`): ``True`` if there's a new chat photo (or it was removed). photo (:tl:`Photo`, optional): The new photo (or ``None`` if it was removed). - user_added (:obj:`bool`): + user_added (`bool`): ``True`` if the user was added by some other. - user_joined (:obj:`bool`): + user_joined (`bool`): ``True`` if the user joined on their own. - user_left (:obj:`bool`): + user_left (`bool`): ``True`` if the user left on their own. - user_kicked (:obj:`bool`): + user_kicked (`bool`): ``True`` if the user was kicked by some other. - created (:obj:`bool`, optional): + created (`bool`, optional): ``True`` if this chat was just created. - new_title (:obj:`bool`, optional): + new_title (`bool`, optional): The new title string for the chat, if applicable. - unpin (:obj:`bool`): + unpin (`bool`): ``True`` if the existing pin gets unpinned. """ def __init__(self, where, new_pin=None, new_photo=None, @@ -820,7 +826,9 @@ class ChatAction(_EventBuilder): if isinstance(self._pinned_message, int) and await self.input_chat: r = await self._client(functions.channels.GetMessagesRequest( - self._input_chat, [self._pinned_message] + self._input_chat, [ + types.InputMessageID(self._pinned_message) + ] )) try: self._pinned_message = next( @@ -941,7 +949,7 @@ class UserUpdate(_EventBuilder): else: return - event._entities = update.entities + event._entities = update._entities return self._filter_event(event) class Event(_EventCommon): @@ -949,62 +957,62 @@ class UserUpdate(_EventBuilder): Represents the event of an user status update (last seen, joined). Members: - online (:obj:`bool`, optional): + online (`bool`, optional): ``True`` if the user is currently online, ``False`` otherwise. Might be ``None`` if this information is not present. - last_seen (:obj:`datetime`, optional): + last_seen (`datetime`, optional): Exact date when the user was last seen if known. - until (:obj:`datetime`, optional): + until (`datetime`, optional): Until when will the user remain online. - within_months (:obj:`bool`): + within_months (`bool`): ``True`` if the user was seen within 30 days. - within_weeks (:obj:`bool`): + within_weeks (`bool`): ``True`` if the user was seen within 7 days. - recently (:obj:`bool`): + recently (`bool`): ``True`` if the user was seen within a day. action (:tl:`SendMessageAction`, optional): The "typing" action if any the user is performing if any. - cancel (:obj:`bool`): + cancel (`bool`): ``True`` if the action was cancelling other actions. - typing (:obj:`bool`): + typing (`bool`): ``True`` if the action is typing a message. - recording (:obj:`bool`): + recording (`bool`): ``True`` if the action is recording something. - uploading (:obj:`bool`): + uploading (`bool`): ``True`` if the action is uploading something. - playing (:obj:`bool`): + playing (`bool`): ``True`` if the action is playing a game. - audio (:obj:`bool`): + audio (`bool`): ``True`` if what's being recorded/uploaded is an audio. - round (:obj:`bool`): + round (`bool`): ``True`` if what's being recorded/uploaded is a round video. - video (:obj:`bool`): + video (`bool`): ``True`` if what's being recorded/uploaded is an video. - document (:obj:`bool`): + document (`bool`): ``True`` if what's being uploaded is document. - geo (:obj:`bool`): + geo (`bool`): ``True`` if what's being uploaded is a geo. - photo (:obj:`bool`): + photo (`bool`): ``True`` if what's being uploaded is a photo. - contact (:obj:`bool`): + contact (`bool`): ``True`` if what's being uploaded (selected) is a contact. """ def __init__(self, user_id, status=None, typing=None): @@ -1090,7 +1098,7 @@ class MessageEdited(NewMessage): else: return - event._entities = update.entities + event._entities = update._entities return self._message_filter_event(event) class Event(NewMessage.Event): @@ -1116,7 +1124,7 @@ class MessageDeleted(_EventBuilder): else: return - event._entities = update.entities + event._entities = update._entities return self._filter_event(event) class Event(_EventCommon): @@ -1128,6 +1136,140 @@ class MessageDeleted(_EventBuilder): self.deleted_ids = deleted_ids +@_name_inner_event +class MessageRead(_EventBuilder): + """ + Event fired when one or more messages have been read. + + Args: + inbox (`bool`, optional): + If this argument is ``True``, then when you read someone else's + messages the event will be fired. By default (``False``) only + when messages you sent are read by someone else will fire it. + """ + def __init__(self, inbox=False, chats=None, blacklist_chats=None): + super().__init__(chats, blacklist_chats) + self.inbox = inbox + + def build(self, update): + if isinstance(update, types.UpdateReadHistoryInbox): + event = MessageRead.Event(update.peer, update.max_id, False) + elif isinstance(update, types.UpdateReadHistoryOutbox): + event = MessageRead.Event(update.peer, update.max_id, True) + elif isinstance(update, types.UpdateReadChannelInbox): + event = MessageRead.Event(types.PeerChannel(update.channel_id), + update.max_id, False) + elif isinstance(update, types.UpdateReadChannelOutbox): + event = MessageRead.Event(types.PeerChannel(update.channel_id), + update.max_id, True) + elif isinstance(update, types.UpdateReadMessagesContents): + event = MessageRead.Event(message_ids=update.messages, + contents=True) + elif isinstance(update, types.UpdateChannelReadMessagesContents): + event = MessageRead.Event(types.PeerChannel(update.channel_id), + message_ids=update.messages, + contents=True) + else: + return + + if self.inbox == event.outbox: + return + + event._entities = update._entities + return self._filter_event(event) + + class Event(_EventCommon): + """ + Represents the event of one or more messages being read. + + Members: + max_id (`int`): + Up to which message ID has been read. Every message + with an ID equal or lower to it have been read. + + outbox (`bool`): + ``True`` if someone else has read your messages. + + contents (`bool`): + ``True`` if what was read were the contents of a message. + This will be the case when e.g. you play a voice note. + It may only be set on ``inbox`` events. + """ + def __init__(self, peer=None, max_id=None, out=False, contents=False, + message_ids=None): + self.outbox = out + self.contents = contents + self._message_ids = message_ids or [] + self._messages = None + self.max_id = max_id or max(message_ids or [], default=None) + super().__init__(peer, self.max_id) + + @property + def inbox(self): + """ + ``True`` if you have read someone else's messages. + """ + return not self.outbox + + @property + def message_ids(self): + """ + The IDs of the messages **which contents'** were read. + + Use :meth:`is_read` if you need to check whether a message + was read instead checking if it's in here. + """ + return self._message_ids + + @property + async def messages(self): + """ + The list of :tl:`Message` **which contents'** were read. + + Use :meth:`is_read` if you need to check whether a message + was read instead checking if it's in here. + """ + if self._messages is None: + chat = self.input_chat + if not chat: + self._messages = [] + elif isinstance(chat, types.InputPeerChannel): + ids = [types.InputMessageID(x) for x in self._message_ids] + self._messages =\ + await self._client(functions.channels.GetMessagesRequest( + chat, ids + )).messages + else: + ids = [types.InputMessageID(x) for x in self._message_ids] + self._messages =\ + await self._client(functions.messages.GetMessagesRequest( + ids + )).messages + + return self._messages + + def is_read(self, message): + """ + Returns ``True`` if the given message (or its ID) has been read. + + If a list-like argument is provided, this method will return a + list of booleans indicating which messages have been read. + """ + if utils.is_list_like(message): + return [(m if isinstance(m, int) else m.id) <= self.max_id + for m in message] + else: + return (message if isinstance(message, int) + else message.id) <= self.max_id + + def __contains__(self, message): + """``True`` if the message(s) are read message.""" + if utils.is_list_like(message): + return all(self.is_read(message)) + else: + return self.is_read(message) + + class StopPropagation(Exception): """ If this exception is raised in any of the handlers for a given event, diff --git a/telethon/network/mtproto_sender.py b/telethon/network/mtproto_sender.py index 00a68472..a1e440a8 100644 --- a/telethon/network/mtproto_sender.py +++ b/telethon/network/mtproto_sender.py @@ -479,11 +479,13 @@ class MtProtoSender: reader.read_int(signed=False) # code request_id = reader.read_long() inner_code = reader.read_int(signed=False) + reader.seek(-4) __log__.debug('Received response for request with ID %d', request_id) request = self._pop_request(request_id) if inner_code == 0x2144ca19: # RPC Error + reader.seek(4) if self.session.report_errors and request: error = rpc_message_to_error( reader.read_int(), reader.tgread_string(), @@ -505,12 +507,10 @@ class MtProtoSender: return True # All contents were read okay elif request: - if inner_code == 0x3072cfa1: # GZip packed - unpacked_data = gzip.decompress(reader.tgread_bytes()) - with BinaryReader(unpacked_data) as compressed_reader: + if inner_code == GzipPacked.CONSTRUCTOR_ID: + with BinaryReader(GzipPacked.read(reader)) as compressed_reader: request.on_response(compressed_reader) else: - reader.seek(-4) request.on_response(reader) self.session.process_entities(request.result) @@ -525,10 +525,17 @@ class MtProtoSender: # session, it will be skipped by the handle_container(). # For some reason this also seems to happen when downloading # photos, where the server responds with FileJpeg(). - try: - obj = reader.tgread_object() - except Exception as e: - obj = '(failed to read: %s)' % e + def _try_read(r): + try: + return r.tgread_object() + except Exception as e: + return '(failed to read: {})'.format(e) + + if inner_code == GzipPacked.CONSTRUCTOR_ID: + with BinaryReader(GzipPacked.read(reader)) as compressed_reader: + obj = _try_read(compressed_reader) + else: + obj = _try_read(reader) __log__.warning( 'Lost request (ID %d) with code %s will be skipped, contents: %s', diff --git a/telethon/telegram_bare_client.py b/telethon/telegram_bare_client.py index f169ae2c..9192d4df 100644 --- a/telethon/telegram_bare_client.py +++ b/telethon/telegram_bare_client.py @@ -9,7 +9,7 @@ from .crypto import rsa from .errors import ( RPCError, BrokenAuthKeyError, ServerError, FloodWaitError, FloodTestPhoneWaitError, TypeNotFoundError, UnauthorizedError, - PhoneMigrateError, NetworkMigrateError, UserMigrateError + PhoneMigrateError, NetworkMigrateError, UserMigrateError, AuthKeyError ) from .network import authenticator, MtProtoSender, Connection, ConnectionMode from .sessions import Session, SQLiteSession @@ -217,6 +217,15 @@ class TelegramBareClient: self.disconnect() return await self.connect(_sync_updates=_sync_updates) + except AuthKeyError as e: + # As of late March 2018 there were two AUTH_KEY_DUPLICATED + # reports. Retrying with a clean auth_key should fix this. + __log__.warning('Auth key error %s. Clearing it and retrying.', e) + self.disconnect() + self.session.auth_key = None + self.session.save() + return self.connect(_sync_updates=_sync_updates) + except (RPCError, ConnectionError) as e: # Probably errors from the previous session, ignore them __log__.error('Connection failed due to %s', e) diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index ad3dce12..5fe5a623 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -38,12 +38,12 @@ from .errors import ( RPCError, UnauthorizedError, PhoneCodeEmptyError, PhoneCodeExpiredError, PhoneCodeHashEmptyError, PhoneCodeInvalidError, LocationInvalidError, SessionPasswordNeededError, FileMigrateError, PhoneNumberUnoccupiedError, - PhoneNumberOccupiedError + PhoneNumberOccupiedError, EmailUnconfirmedError, PasswordEmptyError ) from .network import ConnectionMode from .tl.custom import Draft, Dialog from .tl.functions.account import ( - GetPasswordRequest + GetPasswordRequest, UpdatePasswordSettingsRequest ) from .tl.functions.auth import ( CheckPasswordRequest, LogOutRequest, SendCodeRequest, SignInRequest, @@ -83,9 +83,10 @@ from .tl.types import ( InputMessageEntityMentionName, DocumentAttributeVideo, UpdateEditMessage, UpdateEditChannelMessage, UpdateShort, Updates, MessageMediaWebPage, ChannelParticipantsSearch, PhotoSize, PhotoCachedSize, - PhotoSizeEmpty, MessageService + PhotoSizeEmpty, MessageService, ChatParticipants ) from .tl.types.messages import DialogsSlice +from .tl.types.account import PasswordInputSettings, NoPassword from .extensions import markdown, html __log__ = logging.getLogger(__name__) @@ -96,52 +97,51 @@ class TelegramClient(TelegramBareClient): Initializes the Telegram client with the specified API ID and Hash. Args: - session (:obj:`str` | :obj:`telethon.sessions.abstract.Session`, \ - :obj:`None`): + session (`str` | `telethon.sessions.abstract.Session`, `None`): The file name of the session file to be used if a string is given (it may be a full path), or the Session instance to be used otherwise. If it's ``None``, the session will not be saved, and you should call :meth:`.log_out()` when you're done. - api_id (:obj:`int` | :obj:`str`): + api_id (`int` | `str`): The API ID you obtained from https://my.telegram.org. - api_hash (:obj:`str`): + api_hash (`str`): The API ID you obtained from https://my.telegram.org. - connection_mode (:obj:`ConnectionMode`, optional): + connection_mode (`ConnectionMode`, optional): The connection mode to be used when creating a new connection to the servers. Defaults to the ``TCP_FULL`` mode. This will only affect how messages are sent over the network and how much processing is required before sending them. - use_ipv6 (:obj:`bool`, optional): + use_ipv6 (`bool`, optional): Whether to connect to the servers through IPv6 or not. By default this is ``False`` as IPv6 support is not too widespread yet. - proxy (:obj:`tuple` | :obj:`dict`, optional): + proxy (`tuple` | `dict`, optional): A tuple consisting of ``(socks.SOCKS5, 'host', port)``. See https://github.com/Anorov/PySocks#usage-1 for more. - update_workers (:obj:`int`, optional): + update_workers (`int`, optional): If specified, represents how many extra threads should be spawned to handle incoming updates, and updates will be kept in memory until they are processed. Note that you must set this to at least ``0`` if you want to be able to process updates through :meth:`updates.poll()`. - timeout (:obj:`int` | :obj:`float` | :obj:`timedelta`, optional): + timeout (`int` | `float` | `timedelta`, optional): The timeout to be used when receiving responses from the network. Defaults to 5 seconds. - spawn_read_thread (:obj:`bool`, optional): + spawn_read_thread (`bool`, optional): Whether to use an extra background thread or not. Defaults to ``True`` so receiving items from the network happens instantly, as soon as they arrive. Can still be disabled if you want to run the library without any additional thread. - report_errors (:obj:`bool`, optional): + report_errors (`bool`, optional): Whether to report RPC errors or not. Defaults to ``True``, see :ref:`api-status` for more information. @@ -204,10 +204,10 @@ class TelegramClient(TelegramBareClient): Sends a code request to the specified phone number. Args: - phone (:obj:`str` | :obj:`int`): + phone (`str` | `int`): The phone to which the code will be sent. - force_sms (:obj:`bool`, optional): + force_sms (`bool`, optional): Whether to force sending as SMS. Returns: @@ -247,36 +247,36 @@ class TelegramClient(TelegramBareClient): (You are now logged in) Args: - phone (:obj:`str` | :obj:`int` | :obj:`callable`): + phone (`str` | `int` | `callable`): The phone (or callable without arguments to get it) to which the code will be sent. - password (:obj:`callable`, optional): + password (`callable`, optional): The password for 2 Factor Authentication (2FA). This is only required if it is enabled in your account. - bot_token (:obj:`str`): + bot_token (`str`): Bot Token obtained by `@BotFather `_ to log in as a bot. Cannot be specified with ``phone`` (only one of either allowed). - force_sms (:obj:`bool`, optional): + force_sms (`bool`, optional): Whether to force sending the code request as SMS. This only makes sense when signing in with a `phone`. - code_callback (:obj:`callable`, optional): + code_callback (`callable`, optional): A callable that will be used to retrieve the Telegram login code. Defaults to `input()`. - first_name (:obj:`str`, optional): + first_name (`str`, optional): The first name to be used if signing up. This has no effect if the account already exists and you sign in. - last_name (:obj:`str`, optional): + last_name (`str`, optional): Similar to the first name, but for the last. Optional. Returns: - This :obj:`TelegramClient`, so initialization + This `TelegramClient`, so initialization can be chained with ``.start()``. """ @@ -367,26 +367,26 @@ class TelegramClient(TelegramBareClient): or code that Telegram sent. Args: - phone (:obj:`str` | :obj:`int`): + phone (`str` | `int`): The phone to send the code to if no code was provided, or to override the phone that was previously used with these requests. - code (:obj:`str` | :obj:`int`): + code (`str` | `int`): The code that Telegram sent. Note that if you have sent this code through the application itself it will immediately expire. If you want to send the code, obfuscate it somehow. If you're not doing any of this you can ignore this note. - password (:obj:`str`): + password (`str`): 2FA password, should be used if a previous call raised SessionPasswordNeededError. - bot_token (:obj:`str`): + bot_token (`str`): Used to sign in as a bot. Not all requests will be available. This should be the hash the @BotFather gave you. - phone_code_hash (:obj:`str`): + phone_code_hash (`str`): The hash returned by .send_code_request. This can be set to None to use the last hash known. @@ -443,13 +443,13 @@ class TelegramClient(TelegramBareClient): You must call .send_code_request(phone) first. Args: - code (:obj:`str` | :obj:`int`): + code (`str` | `int`): The code sent by Telegram - first_name (:obj:`str`): + first_name (`str`): The first name to be used by the new account. - last_name (:obj:`str`, optional) + last_name (`str`, optional) Optional last name. Returns: @@ -495,7 +495,7 @@ class TelegramClient(TelegramBareClient): or None if the request fails (hence, not authenticated). Args: - input_peer (:obj:`bool`, optional): + input_peer (`bool`, optional): Whether to return the :tl:`InputPeerUser` version or the normal :tl:`User`. This can be useful if you just need to know the ID of yourself. @@ -527,27 +527,27 @@ class TelegramClient(TelegramBareClient): Dialogs are the open "chats" or conversations with other people. Args: - limit (:obj:`int` | :obj:`None`): + limit (`int` | `None`): How many dialogs to be retrieved as maximum. Can be set to ``None`` to retrieve all dialogs. Note that this may take whole minutes if you have hundreds of dialogs, as Telegram will tell the library to slow down through a ``FloodWaitError``. - offset_date (:obj:`datetime`, optional): + offset_date (`datetime`, optional): The offset date to be used. - offset_id (:obj:`int`, optional): + offset_id (`int`, optional): The message ID to be used as an offset. offset_peer (:tl:`InputPeer`, optional): The peer to be used as an offset. - _total (:obj:`list`, optional): + _total (`list`, optional): A single-item list to pass the total parameter by reference. Yields: - Instances of :obj:`telethon.tl.custom.dialog.Dialog`. + Instances of `telethon.tl.custom.dialog.Dialog`. """ limit = float('inf') if limit is None else int(limit) if limit == 0: @@ -617,9 +617,9 @@ class TelegramClient(TelegramBareClient): """ Iterator over all open draft messages. - Instances of :obj:`telethon.tl.custom.draft.Draft` are yielded. - You can call :obj:`telethon.tl.custom.draft.Draft.set_message` - to change the message or :obj:`telethon.tl.custom.draft.Draft.delete` + Instances of `telethon.tl.custom.draft.Draft` are yielded. + You can call `telethon.tl.custom.draft.Draft.set_message` + to change the message or `telethon.tl.custom.draft.Draft.delete` among other things. """ for update in (await self(GetAllDraftsRequest())).updates: @@ -710,33 +710,33 @@ class TelegramClient(TelegramBareClient): Sends the given message to the specified entity (user/chat/channel). Args: - entity (:obj:`entity`): + entity (`entity`): To who will it be sent. - message (:obj:`str` | :tl:`Message`): + message (`str` | :tl:`Message`): The message to be sent, or another message object to resend. - reply_to (:obj:`int` | :tl:`Message`, optional): + reply_to (`int` | :tl:`Message`, optional): Whether to reply to a message or not. If an integer is provided, it should be the ID of the message that it should reply to. - parse_mode (:obj:`str`, optional): + parse_mode (`str`, optional): Can be 'md' or 'markdown' for markdown-like parsing (default), or 'htm' or 'html' for HTML-like parsing. If ``None`` or any other false-y value is provided, the message will be sent with no formatting. - link_preview (:obj:`bool`, optional): + link_preview (`bool`, optional): Should the link preview be shown? - file (:obj:`file`, optional): + file (`file`, optional): Sends a message with a file attached (e.g. a photo, video, audio or document). The ``message`` may be empty. - force_document (:obj:`bool`, optional): + force_document (`bool`, optional): Whether to send the given file as a document or not. - clear_draft (:obj:`bool`, optional): + clear_draft (`bool`, optional): Whether the existing draft should be cleared or not. Has no effect when sending a file. @@ -805,13 +805,13 @@ class TelegramClient(TelegramBareClient): Forwards the given message(s) to the specified entity. Args: - entity (:obj:`entity`): + entity (`entity`): To which entity the message(s) will be forwarded. - messages (:obj:`list` | :obj:`int` | :tl:`Message`): + messages (`list` | `int` | :tl:`Message`): The message(s) to forward, or their integer IDs. - from_peer (:obj:`entity`): + from_peer (`entity`): If the given messages are integer IDs and not instances of the ``Message`` class, this *must* be specified in order for the forward to work. @@ -858,22 +858,22 @@ class TelegramClient(TelegramBareClient): Edits the given message ID (to change its contents or disable preview). Args: - entity (:obj:`entity`): + entity (`entity`): From which chat to edit the message. - message_id (:obj:`str`): + message_id (`str`): The ID of the message (or ``Message`` itself) to be edited. - message (:obj:`str`, optional): + message (`str`, optional): The new text of the message. - parse_mode (:obj:`str`, optional): + parse_mode (`str`, optional): Can be 'md' or 'markdown' for markdown-like parsing (default), or 'htm' or 'html' for HTML-like parsing. If ``None`` or any other false-y value is provided, the message will be sent with no formatting. - link_preview (:obj:`bool`, optional): + link_preview (`bool`, optional): Should the link preview be shown? Raises: @@ -902,15 +902,15 @@ class TelegramClient(TelegramBareClient): Deletes a message from a chat, optionally "for everyone". Args: - entity (:obj:`entity`): + entity (`entity`): From who the message will be deleted. This can actually be ``None`` for normal chats, but **must** be present for channels and megagroups. - message_ids (:obj:`list` | :obj:`int` | :tl:`Message`): + message_ids (`list` | `int` | :tl:`Message`): The IDs (or ID) or messages to be deleted. - revoke (:obj:`bool`, optional): + revoke (`bool`, optional): Whether the message should be deleted for everyone or not. By default it has the opposite behaviour of official clients, and it will delete the message for everyone. @@ -944,48 +944,48 @@ class TelegramClient(TelegramBareClient): Iterator over the message history for the specified entity. Args: - entity (:obj:`entity`): + entity (`entity`): The entity from whom to retrieve the message history. - limit (:obj:`int` | :obj:`None`, optional): + limit (`int` | `None`, optional): Number of messages to be retrieved. Due to limitations with the API retrieving more than 3000 messages will take longer than half a minute (or even more based on previous calls). The limit may also be ``None``, which would eventually return the whole history. - offset_date (:obj:`datetime`): + offset_date (`datetime`): Offset date (messages *previous* to this date will be retrieved). Exclusive. - offset_id (:obj:`int`): + offset_id (`int`): Offset message ID (only messages *previous* to the given ID will be retrieved). Exclusive. - max_id (:obj:`int`): + max_id (`int`): All the messages with a higher (newer) ID or equal to this will be excluded - min_id (:obj:`int`): + min_id (`int`): All the messages with a lower (older) ID or equal to this will be excluded. - add_offset (:obj:`int`): + add_offset (`int`): Additional message offset (all of the specified offsets + this offset = older messages). - batch_size (:obj:`int`): + batch_size (`int`): Messages will be returned in chunks of this size (100 is the maximum). While it makes no sense to modify this value, you are still free to do so. - wait_time (:obj:`int`): + wait_time (`int`): Wait time between different :tl:`GetHistoryRequest`. Use this parameter to avoid hitting the ``FloodWaitError`` as needed. If left to ``None``, it will default to 1 second only if the limit is higher than 3000. - _total (:obj:`list`, optional): + _total (`list`, optional): A single-item list to pass the total parameter by reference. Yields: @@ -1103,17 +1103,17 @@ class TelegramClient(TelegramBareClient): read their messages, also known as the "double check"). Args: - entity (:obj:`entity`): + entity (`entity`): The chat where these messages are located. - message (:obj:`list` | :tl:`Message`): + message (`list` | :tl:`Message`): Either a list of messages or a single message. - max_id (:obj:`int`): + max_id (`int`): Overrides messages, until which message should the acknowledge should be sent. - clear_mentions (:obj:`bool`): + clear_mentions (`bool`): Whether the mention badge should be cleared (so that there are no more mentions) or not for the given entity. @@ -1168,13 +1168,13 @@ class TelegramClient(TelegramBareClient): Iterator over the participants belonging to the specified chat. Args: - entity (:obj:`entity`): + entity (`entity`): The entity from which to retrieve the participants list. - limit (:obj:`int`): + limit (`int`): Limits amount of participants fetched. - search (:obj:`str`, optional): + search (`str`, optional): Look for participants with this string in name/username. filter (:tl:`ChannelParticipantsFilter`, optional): @@ -1182,7 +1182,7 @@ class TelegramClient(TelegramBareClient): Note that you might not have permissions for some filter. This has no effect for normal chats or users. - aggressive (:obj:`bool`, optional): + aggressive (`bool`, optional): Aggressively looks for all participants in the chat in order to get more than 10,000 members (a hard limit imposed by Telegram). Note that this might take a long @@ -1192,7 +1192,7 @@ class TelegramClient(TelegramBareClient): This has no effect for groups or channels with less than 10,000 members, or if a ``filter`` is given. - _total (:obj:`list`, optional): + _total (`list`, optional): A single-item list to pass the total parameter by reference. Yields: @@ -1282,6 +1282,11 @@ class TelegramClient(TelegramBareClient): elif isinstance(entity, InputPeerChat): # TODO We *could* apply the `filter` here ourselves full = await self(GetFullChatRequest(entity.chat_id)) + if not isinstance(full.full_chat.participants, ChatParticipants): + # ChatParticipantsForbidden won't have ``.participants`` + _total[0] = 0 + return + if _total: _total[0] = len(full.full_chat.participants.participants) @@ -1336,10 +1341,10 @@ class TelegramClient(TelegramBareClient): Sends a file to the specified entity. Args: - entity (:obj:`entity`): + entity (`entity`): Who will receive the file. - file (:obj:`str` | :obj:`bytes` | :obj:`file` | :obj:`media`): + file (`str` | `bytes` | `file` | `media`): The path of the file, byte array, or stream that will be sent. Note that if a byte array or a stream is given, a filename or its type won't be inferred, and it will be sent as an @@ -1356,35 +1361,35 @@ class TelegramClient(TelegramBareClient): sent as an album in the order in which they appear, sliced in chunks of 10 if more than 10 are given. - caption (:obj:`str`, optional): + caption (`str`, optional): Optional caption for the sent media message. - force_document (:obj:`bool`, optional): + force_document (`bool`, optional): If left to ``False`` and the file is a path that ends with the extension of an image file or a video file, it will be sent as such. Otherwise always as a document. - progress_callback (:obj:`callable`, optional): + progress_callback (`callable`, optional): A callback function accepting two parameters: ``(sent bytes, total)``. - reply_to (:obj:`int` | :tl:`Message`): + reply_to (`int` | :tl:`Message`): Same as reply_to from .send_message(). - attributes (:obj:`list`, optional): + attributes (`list`, optional): Optional attributes that override the inferred ones, like :tl:`DocumentAttributeFilename` and so on. - thumb (:obj:`str` | :obj:`bytes` | :obj:`file`, optional): + thumb (`str` | `bytes` | `file`, optional): Optional thumbnail (for videos). - allow_cache (:obj:`bool`, optional): + allow_cache (`bool`, optional): Whether to allow using the cached version stored in the database or not. Defaults to ``True`` to avoid re-uploads. Must be ``False`` if you wish to use different attributes or thumb than those that were used when the file was cached. - parse_mode (:obj:`str`, optional): + parse_mode (`str`, optional): The parse mode for the caption message. Kwargs: @@ -1624,7 +1629,7 @@ class TelegramClient(TelegramBareClient): will **not** upload the file to your own chat or any chat at all. Args: - file (:obj:`str` | :obj:`bytes` | :obj:`file`): + file (`str` | `bytes` | `file`): The path of the file, byte array, or stream that will be sent. Note that if a byte array or a stream is given, a filename or its type won't be inferred, and it will be sent as an @@ -1633,23 +1638,23 @@ class TelegramClient(TelegramBareClient): Subsequent calls with the very same file will result in immediate uploads, unless ``.clear_file_cache()`` is called. - part_size_kb (:obj:`int`, optional): + part_size_kb (`int`, optional): Chunk size when uploading files. The larger, the less requests will be made (up to 512KB maximum). - file_name (:obj:`str`, optional): + file_name (`str`, optional): The file name which will be used on the resulting InputFile. If not specified, the name will be taken from the ``file`` and if this is not a ``str``, it will be ``"unnamed"``. - use_cache (:obj:`type`, optional): + use_cache (`type`, optional): The type of cache to use (currently either ``InputDocument`` or ``InputPhoto``). If present and the file is small enough to need the MD5, it will be checked against the database, and if a match is found, the upload won't be made. Instead, an instance of type ``use_cache`` will be returned. - progress_callback (:obj:`callable`, optional): + progress_callback (`callable`, optional): A callback function accepting two parameters: ``(sent bytes, total)``. @@ -1752,14 +1757,14 @@ class TelegramClient(TelegramBareClient): Downloads the profile photo of the given entity (user/chat/channel). Args: - entity (:obj:`entity`): + entity (`entity`): From who the photo will be downloaded. - file (:obj:`str` | :obj:`file`, optional): + file (`str` | `file`, optional): The output file path, directory, or stream-like object. If the path exists and is a file, it will be overwritten. - download_big (:obj:`bool`, optional): + download_big (`bool`, optional): Whether to use the big version of the available photos. Returns: @@ -1841,11 +1846,11 @@ class TelegramClient(TelegramBareClient): message (:tl:`Message` | :tl:`Media`): The media or message containing the media that will be downloaded. - file (:obj:`str` | :obj:`file`, optional): + file (`str` | `file`, optional): The output file path, directory, or stream-like object. If the path exists and is a file, it will be overwritten. - progress_callback (:obj:`callable`, optional): + progress_callback (`callable`, optional): A callback function accepting two parameters: ``(received bytes, total)``. @@ -2066,19 +2071,19 @@ class TelegramClient(TelegramBareClient): input_location (:tl:`InputFileLocation`): The file location from which the file will be downloaded. - file (:obj:`str` | :obj:`file`): + file (`str` | `file`): The output file path, directory, or stream-like object. If the path exists and is a file, it will be overwritten. - part_size_kb (:obj:`int`, optional): + part_size_kb (`int`, optional): Chunk size when downloading files. The larger, the less requests will be made (up to 512KB maximum). - file_size (:obj:`int`, optional): + file_size (`int`, optional): The file size that is about to be downloaded, if known. Only used if ``progress_callback`` is specified. - progress_callback (:obj:`callable`, optional): + progress_callback (`callable`, optional): A callback function accepting two parameters: ``(downloaded bytes, total)``. Note that the ``total`` is the provided ``file_size``. @@ -2172,7 +2177,7 @@ class TelegramClient(TelegramBareClient): Decorator helper method around add_event_handler(). Args: - event (:obj:`_EventBuilder` | :obj:`type`): + event (`_EventBuilder` | `type`): The event builder class or instance to be used, for instance ``events.NewMessage``. """ @@ -2208,10 +2213,10 @@ class TelegramClient(TelegramBareClient): Registers the given callback to be called on the specified event. Args: - callback (:obj:`callable`): + callback (`callable`): The callable function accepting one parameter to be used. - event (:obj:`_EventBuilder` | :obj:`type`, optional): + event (`_EventBuilder` | `type`, optional): The event builder class or instance to be used, for instance ``events.NewMessage``. @@ -2286,7 +2291,7 @@ class TelegramClient(TelegramBareClient): """ Turns the given entity into a valid Telegram user or chat. - entity (:obj:`str` | :obj:`int` | :tl:`Peer` | :tl:`InputPeer`): + entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`): The entity (or iterable of entities) to be transformed. If it's a string which can be converted to an integer or starts with '+' it will be resolved as if it were a phone number. @@ -2402,7 +2407,7 @@ class TelegramClient(TelegramBareClient): use this kind of InputUser, InputChat and so on, so this is the most suitable call to make for those cases. - entity (:obj:`str` | :obj:`int` | :tl:`Peer` | :tl:`InputPeer`): + entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`): The integer ID of an user or otherwise either of a :tl:`PeerUser`, :tl:`PeerChat` or :tl:`PeerChannel`, for which to get its ``Input*`` version. @@ -2414,6 +2419,9 @@ class TelegramClient(TelegramBareClient): Returns: :tl:`InputPeerUser`, :tl:`InputPeerChat` or :tl:`InputPeerChannel`. """ + if peer in ('me', 'self'): + return InputPeerSelf() + try: # First try to get the entity from cache, otherwise figure it out return self.session.get_input_entity(peer) @@ -2421,8 +2429,6 @@ class TelegramClient(TelegramBareClient): pass if isinstance(peer, str): - if peer in ('me', 'self'): - return InputPeerSelf() return utils.get_input_peer(await self._get_entity_from_string(peer)) original_peer = peer @@ -2459,4 +2465,75 @@ class TelegramClient(TelegramBareClient): 'Make sure you have encountered this peer before.'.format(peer) ) + async def edit_2fa(self, current_password=None, new_password=None, hint='', + email=None): + """ + Changes the 2FA settings of the logged in user, according to the + passed parameters. Take note of the parameter explanations. + + Has no effect if both current and new password are omitted. + + current_password (`str`, optional): + The current password, to authorize changing to ``new_password``. + Must be set if changing existing 2FA settings. + Must **not** be set if 2FA is currently disabled. + Passing this by itself will remove 2FA (if correct). + + new_password (`str`, optional): + The password to set as 2FA. + If 2FA was already enabled, ``current_password`` **must** be set. + Leaving this blank or ``None`` will remove the password. + + hint (`str`, optional): + Hint to be displayed by Telegram when it asks for 2FA. + Leaving unspecified is highly discouraged. + Has no effect if ``new_password`` is not set. + + email (`str`, optional): + Recovery and verification email. Raises ``EmailUnconfirmedError`` + if value differs from current one, and has no effect if + ``new_password`` is not set. + + Returns: + ``True`` if successful, ``False`` otherwise. + """ + if new_password is None and current_password is None: + return False + + pass_result = await self(GetPasswordRequest()) + if isinstance(pass_result, NoPassword) and current_password: + current_password = None + + salt_random = os.urandom(8) + salt = pass_result.new_salt + salt_random + if not current_password: + current_password_hash = salt + else: + current_password = pass_result.current_salt +\ + current_password.encode() + pass_result.current_salt + current_password_hash = hashlib.sha256(current_password).digest() + + if new_password: # Setting new password + new_password = salt + new_password.encode('utf-8') + salt + new_password_hash = hashlib.sha256(new_password).digest() + new_settings = PasswordInputSettings( + new_salt=salt, + new_password_hash=new_password_hash, + hint=hint + ) + if email: # If enabling 2FA or changing email + new_settings.email = email # TG counts empty string as None + return await self(UpdatePasswordSettingsRequest( + current_password_hash, new_settings=new_settings + )) + else: # Removing existing password + return await self(UpdatePasswordSettingsRequest( + current_password_hash, + new_settings=PasswordInputSettings( + new_salt=bytes(), + new_password_hash=bytes(), + hint=hint + ) + )) + # endregion diff --git a/telethon/tl/custom/dialog.py b/telethon/tl/custom/dialog.py index a51e0709..ab115d34 100644 --- a/telethon/tl/custom/dialog.py +++ b/telethon/tl/custom/dialog.py @@ -1,4 +1,5 @@ from . import Draft +from .. import TLObject from ... import utils @@ -13,7 +14,7 @@ class Dialog: dialog (:tl:`Dialog`): The original ``Dialog`` instance. - pinned (:obj:`bool`): + pinned (`bool`): Whether this dialog is pinned to the top or not. message (:tl:`Message`): @@ -21,31 +22,31 @@ class Dialog: will not be updated when new messages arrive, it's only set on creation of the instance. - date (:obj:`datetime`): + date (`datetime`): The date of the last message sent on this dialog. - entity (:obj:`entity`): + entity (`entity`): The entity that belongs to this dialog (user, chat or channel). input_entity (:tl:`InputPeer`): Input version of the entity. - id (:obj:`int`): + id (`int`): The marked ID of the entity, which is guaranteed to be unique. - name (:obj:`str`): + name (`str`): Display name for this dialog. For chats and channels this is their title, and for users it's "First-Name Last-Name". - unread_count (:obj:`int`): + unread_count (`int`): How many messages are currently unread in this dialog. Note that this value won't update when new messages arrive. - unread_mentions_count (:obj:`int`): + unread_mentions_count (`int`): How many mentions are currently unread in this dialog. Note that this value won't update when new messages arrive. - draft (:obj:`telethon.tl.custom.draft.Draft`): + draft (`telethon.tl.custom.draft.Draft`): The draft object in this dialog. It will not be ``None``, so you can call ``draft.set_message(...)``. """ @@ -73,3 +74,19 @@ class Dialog: ``client.send_message(dialog.input_entity, *args, **kwargs)``. """ return await self._client.send_message(self.input_entity, *args, **kwargs) + + def to_dict(self): + return { + '_': 'Dialog', + 'name': self.name, + 'date': self.date, + 'draft': self.draft, + 'message': self.message, + 'entity': self.entity, + } + + def __str__(self): + return TLObject.pretty_format(self.to_dict()) + + def stringify(self): + return TLObject.pretty_format(self.to_dict(), indent=0) diff --git a/telethon/tl/custom/draft.py b/telethon/tl/custom/draft.py index aaeeb743..88aec35a 100644 --- a/telethon/tl/custom/draft.py +++ b/telethon/tl/custom/draft.py @@ -1,7 +1,9 @@ import datetime +from .. import TLObject from ..functions.messages import SaveDraftRequest from ..types import UpdateDraftMessage, DraftMessage +from ...errors import RPCError from ...extensions import markdown @@ -12,13 +14,13 @@ class Draft: instances of this class when calling :meth:`get_drafts()`. Args: - date (:obj:`datetime`): + date (`datetime`): The date of the draft. - link_preview (:obj:`bool`): + link_preview (`bool`): Whether the link preview is enabled or not. - reply_to_msg_id (:obj:`int`): + reply_to_msg_id (`int`): The message ID that the draft will reply to. """ def __init__(self, client, peer, draft): @@ -142,3 +144,24 @@ class Draft: Deletes this draft, and returns ``True`` on success. """ return await self.set_message(text='') + + def to_dict(self): + try: + entity = self.entity + except RPCError as e: + entity = e + + return { + '_': 'Draft', + 'text': self.text, + 'entity': entity, + 'date': self.date, + 'link_preview': self.link_preview, + 'reply_to_msg_id': self.reply_to_msg_id + } + + def __str__(self): + return TLObject.pretty_format(self.to_dict()) + + def stringify(self): + return TLObject.pretty_format(self.to_dict(), indent=0) diff --git a/telethon/update_state.py b/telethon/update_state.py index 128ca3ff..b74ddd7b 100644 --- a/telethon/update_state.py +++ b/telethon/update_state.py @@ -1,9 +1,9 @@ -import logging -import pickle import asyncio -from collections import deque +import itertools +import logging from datetime import datetime +from . import utils from .tl import types as tl __log__ = logging.getLogger(__name__) @@ -42,14 +42,20 @@ class UpdateState: # After running the script for over an hour and receiving over # 1000 updates, the only duplicates received were users going # online or offline. We can trust the server until new reports. + # This should only be used as read-only. if isinstance(update, tl.UpdateShort): + update.update._entities = {} self.handle_update(update.update) # Expand "Updates" into "Update", and pass these to callbacks. # Since .users and .chats have already been processed, we # don't need to care about those either. elif isinstance(update, (tl.Updates, tl.UpdatesCombined)): + entities = {utils.get_peer_id(x): x for x in + itertools.chain(update.users, update.chats)} for u in update.updates: + u._entities = entities self.handle_update(u) # TODO Handle "tl.UpdatesTooLong" else: + update._entities = {} self.handle_update(update) diff --git a/telethon/utils.py b/telethon/utils.py index faa1537a..2427ce13 100644 --- a/telethon/utils.py +++ b/telethon/utils.py @@ -261,12 +261,22 @@ def get_input_media(media, is_photo=False): ttl_seconds=media.ttl_seconds ) + if isinstance(media, (Photo, photos.Photo, PhotoEmpty)): + return InputMediaPhoto( + id=get_input_photo(media) + ) + if isinstance(media, MessageMediaDocument): return InputMediaDocument( id=get_input_document(media.document), ttl_seconds=media.ttl_seconds ) + if isinstance(media, (Document, DocumentEmpty)): + return InputMediaDocument( + id=get_input_document(media) + ) + if isinstance(media, FileLocation): if is_photo: return InputMediaUploadedPhoto(file=media) diff --git a/telethon/version.py b/telethon/version.py index 90266dbf..20c74dd3 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__ = '0.18.1' +__version__ = '0.18.2' diff --git a/telethon_generator/error_generator.py b/telethon_generator/error_generator.py index 73fb5c5a..4aad78ec 100644 --- a/telethon_generator/error_generator.py +++ b/telethon_generator/error_generator.py @@ -11,6 +11,7 @@ known_base_classes = { 401: 'UnauthorizedError', 403: 'ForbiddenError', 404: 'NotFoundError', + 406: 'AuthKeyError', 420: 'FloodError', 500: 'ServerError', } diff --git a/telethon_generator/parser/source_builder.py b/telethon_generator/parser/source_builder.py index 2b62cf61..9fb61593 100644 --- a/telethon_generator/parser/source_builder.py +++ b/telethon_generator/parser/source_builder.py @@ -16,7 +16,7 @@ class SourceBuilder: """ self.write(' ' * (self.current_indent * self.indent_size)) - def write(self, string): + def write(self, string, *args, **kwargs): """Writes a string into the source code, applying indentation if required """ @@ -26,13 +26,16 @@ class SourceBuilder: if string.strip(): self.indent() - self.out_stream.write(string) + if args or kwargs: + self.out_stream.write(string.format(*args, **kwargs)) + else: + self.out_stream.write(string) - def writeln(self, string=''): + def writeln(self, string='', *args, **kwargs): """Writes a string into the source code _and_ appends a new line, applying indentation if required """ - self.write(string + '\n') + self.write(string + '\n', *args, **kwargs) self.on_new_line = True # If we're writing a block, increment indent for the next time diff --git a/telethon_generator/scheme.tl b/telethon_generator/scheme.tl index a736b066..2ed348da 100644 --- a/telethon_generator/scheme.tl +++ b/telethon_generator/scheme.tl @@ -556,7 +556,7 @@ accountDaysTTL#b8d0afdf days:int = AccountDaysTTL; documentAttributeImageSize#6c37c15c w:int h:int = DocumentAttribute; documentAttributeAnimated#11b58939 = DocumentAttribute; documentAttributeSticker#6319d612 flags:# mask:flags.1?true alt:string stickerset:InputStickerSet mask_coords:flags.0?MaskCoords = DocumentAttribute; -documentAttributeVideo#ef02ce6 flags:# round_message:flags.0?true duration:int w:int h:int = DocumentAttribute; +documentAttributeVideo#ef02ce6 flags:# round_message:flags.0?true supports_streaming:flags.1?true duration:int w:int h:int = DocumentAttribute; documentAttributeAudio#9852f9c6 flags:# voice:flags.10?true duration:int title:flags.0?string performer:flags.1?string waveform:flags.2?bytes = DocumentAttribute; documentAttributeFilename#15590068 file_name:string = DocumentAttribute; documentAttributeHasStickers#9801d2f7 = DocumentAttribute; @@ -938,7 +938,15 @@ recentMeUrlStickerSet#bc0a57dc url:string set:StickerSetCovered = RecentMeUrl; help.recentMeUrls#e0310d7 urls:Vector chats:Vector users:Vector = help.RecentMeUrls; -inputSingleMedia#31bc3d25 media:InputMedia flags:# random_id:long message:string entities:flags.0?Vector = InputSingleMedia; +inputSingleMedia#1cc6e91f flags:# media:InputMedia random_id:long message:string entities:flags.0?Vector = InputSingleMedia; + +webAuthorization#cac943f2 hash:long bot_id:int domain:string browser:string platform:string date_created:int date_active:int ip:string region:string = WebAuthorization; + +account.webAuthorizations#ed56c9fc authorizations:Vector users:Vector = account.WebAuthorizations; + +inputMessageID#a676a322 id:int = InputMessage; +inputMessageReplyTo#bad88395 id:int = InputMessage; +inputMessagePinned#86872538 = InputMessage; ---functions--- @@ -993,6 +1001,9 @@ account.updatePasswordSettings#fa7c4b86 current_password_hash:bytes new_settings account.sendConfirmPhoneCode#1516d7bd flags:# allow_flashcall:flags.0?true hash:string current_number:flags.0?Bool = auth.SentCode; account.confirmPhone#5f2178c3 phone_code_hash:string phone_code:string = Bool; account.getTmpPassword#4a82327e password_hash:bytes period:int = account.TmpPassword; +account.getWebAuthorizations#182e6d6f = account.WebAuthorizations; +account.resetWebAuthorization#2d01b9ef hash:long = Bool; +account.resetWebAuthorizations#682d2594 = Bool; users.getUsers#d91a548 id:Vector = Vector; users.getFullUser#ca30a5b1 id:InputUser = UserFull; @@ -1013,7 +1024,7 @@ contacts.getTopPeers#d4982db5 flags:# correspondents:flags.0?true bots_pm:flags. contacts.resetTopPeerRating#1ae373ac category:TopPeerCategory peer:InputPeer = Bool; contacts.resetSaved#879537f1 = Bool; -messages.getMessages#4222fa74 id:Vector = messages.Messages; +messages.getMessages#63c66506 id:Vector = messages.Messages; messages.getDialogs#191ba9c5 flags:# exclude_pinned:flags.0?true offset_date:int offset_id:int offset_peer:InputPeer limit: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#39e9ea0 flags:# peer:InputPeer q:string from_id:flags.0?InputUser filter:MessagesFilter min_date:int max_date:int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; @@ -1141,7 +1152,7 @@ channels.readHistory#cc104937 channel:InputChannel max_id:int = Bool; channels.deleteMessages#84c1fd4e channel:InputChannel id:Vector = messages.AffectedMessages; channels.deleteUserHistory#d10dd71b channel:InputChannel user_id:InputUser = messages.AffectedHistory; channels.reportSpam#fe087810 channel:InputChannel user_id:InputUser id:Vector = Bool; -channels.getMessages#93d7b347 channel:InputChannel id:Vector = messages.Messages; +channels.getMessages#ad8c9a23 channel:InputChannel id:Vector = messages.Messages; channels.getParticipants#123e05e9 channel:InputChannel filter:ChannelParticipantsFilter offset:int limit:int hash:int = channels.ChannelParticipants; channels.getParticipant#546dd7a6 channel:InputChannel user_id:InputUser = channels.ChannelParticipant; channels.getChannels#a7f6bbb id:Vector = messages.Chats; diff --git a/telethon_generator/tl_generator.py b/telethon_generator/tl_generator.py index cb5c7700..abc2301a 100644 --- a/telethon_generator/tl_generator.py +++ b/telethon_generator/tl_generator.py @@ -24,9 +24,11 @@ class TLGenerator: self.output_dir = output_dir def _get_file(self, *paths): + """Wrapper around ``os.path.join()`` with output as first path.""" return os.path.join(self.output_dir, *paths) def _rm_if_exists(self, filename): + """Recursively deletes the given filename if it exists.""" file = self._get_file(filename) if os.path.exists(file): if os.path.isdir(file): @@ -35,19 +37,21 @@ class TLGenerator: os.remove(file) def tlobjects_exist(self): - """Determines whether the TLObjects were previously - generated (hence exist) or not + """ + Determines whether the TLObjects were previously + generated (hence exist) or not. """ return os.path.isfile(self._get_file('all_tlobjects.py')) def clean_tlobjects(self): - """Cleans the automatically generated TLObjects from disk""" + """Cleans the automatically generated TLObjects from disk.""" for name in ('functions', 'types', 'all_tlobjects.py'): self._rm_if_exists(name) def generate_tlobjects(self, scheme_file, import_depth): - """Generates all the TLObjects from scheme.tl to - tl/functions and tl/types + """ + Generates all the TLObjects from the ``scheme_file`` to + ``tl/functions`` and ``tl/types``. """ # First ensure that the required parent directories exist @@ -85,42 +89,33 @@ class TLGenerator: # Step 4: Once all the objects have been generated, # we can now group them in a single file filename = os.path.join(self._get_file('all_tlobjects.py')) - with open(filename, 'w', encoding='utf-8') as file: - with SourceBuilder(file) as builder: - builder.writeln(AUTO_GEN_NOTICE) - builder.writeln() + with open(filename, 'w', encoding='utf-8') as file,\ + SourceBuilder(file) as builder: + builder.writeln(AUTO_GEN_NOTICE) + builder.writeln() - builder.writeln('from . import types, functions') - builder.writeln() + builder.writeln('from . import types, functions') + builder.writeln() - # Create a constant variable to indicate which layer this is - builder.writeln('LAYER = {}'.format( - TLParser.find_layer(scheme_file)) - ) - builder.writeln() + # Create a constant variable to indicate which layer this is + builder.writeln('LAYER = {}', TLParser.find_layer(scheme_file)) + builder.writeln() - # Then create the dictionary containing constructor_id: class - builder.writeln('tlobjects = {') - builder.current_indent += 1 + # Then create the dictionary containing constructor_id: class + builder.writeln('tlobjects = {') + builder.current_indent += 1 - # Fill the dictionary (0x1a2b3c4f: tl.full.type.path.Class) - for tlobject in tlobjects: - constructor = hex(tlobject.id) - if len(constructor) != 10: - # Make it a nice length 10 so it fits well - constructor = '0x' + constructor[2:].zfill(8) + # Fill the dictionary (0x1a2b3c4f: tl.full.type.path.Class) + for tlobject in tlobjects: + builder.write('{:#010x}: ', tlobject.id) + builder.write('functions' if tlobject.is_function else 'types') + if tlobject.namespace: + builder.write('.' + tlobject.namespace) - builder.write('{}: '.format(constructor)) - builder.write( - 'functions' if tlobject.is_function else 'types') + builder.writeln('.{},', tlobject.class_name()) - if tlobject.namespace: - builder.write('.' + tlobject.namespace) - - builder.writeln('.{},'.format(tlobject.class_name())) - - builder.current_indent -= 1 - builder.writeln('}') + builder.current_indent -= 1 + builder.writeln('}') @staticmethod def _write_init_py(out_dir, depth, namespace_tlobjects, type_constructors): @@ -136,16 +131,17 @@ class TLGenerator: # so they all can be serialized and sent, however, only the # functions are "content_related". builder.writeln( - 'from {}.tl.tlobject import TLObject'.format('.' * depth) + 'from {}.tl.tlobject import TLObject', '.' * depth ) - builder.writeln('from typing import Optional, List, Union, TYPE_CHECKING') + builder.writeln('from typing import Optional, List, ' + 'Union, TYPE_CHECKING') # Add the relative imports to the namespaces, # unless we already are in a namespace. if not ns: - builder.writeln('from . import {}'.format(', '.join( + builder.writeln('from . import {}', ', '.join( x for x in namespace_tlobjects.keys() if x - ))) + )) # Import 'os' for those needing access to 'os.urandom()' # Currently only 'random_id' needs 'os' to be imported, @@ -204,18 +200,18 @@ class TLGenerator: if name == 'date': imports['datetime'] = ['datetime'] continue - elif not import_space in imports: + elif import_space not in imports: imports[import_space] = set() imports[import_space].add('Type{}'.format(name)) - # Add imports required for type checking. - builder.writeln('if TYPE_CHECKING:') - for namespace, names in imports.items(): - builder.writeln('from {} import {}'.format( - namespace, ', '.join(names))) - else: - builder.writeln('pass') - builder.end_block() + # Add imports required for type checking + if imports: + builder.writeln('if TYPE_CHECKING:') + for namespace, names in imports.items(): + builder.writeln('from {} import {}', + namespace, ', '.join(names)) + + builder.end_block() # Generate the class for every TLObject for t in tlobjects: @@ -229,25 +225,24 @@ class TLGenerator: for line in type_defs: builder.writeln(line) - @staticmethod def _write_source_code(tlobject, builder, depth, type_constructors): - """Writes the source code corresponding to the given TLObject - by making use of the 'builder' SourceBuilder. + """ + Writes the source code corresponding to the given TLObject + by making use of the ``builder`` `SourceBuilder`. - Additional information such as file path depth and - the Type: [Constructors] must be given for proper - importing and documentation strings. + Additional information such as file path depth and + the ``Type: [Constructors]`` must be given for proper + importing and documentation strings. """ builder.writeln() builder.writeln() - builder.writeln('class {}(TLObject):'.format(tlobject.class_name())) + builder.writeln('class {}(TLObject):', tlobject.class_name()) # Class-level variable to store its Telegram's constructor ID - builder.writeln('CONSTRUCTOR_ID = {}'.format(hex(tlobject.id))) - builder.writeln('SUBCLASS_OF_ID = {}'.format( - hex(crc32(tlobject.result.encode('ascii')))) - ) + builder.writeln('CONSTRUCTOR_ID = {:#x}', tlobject.id) + builder.writeln('SUBCLASS_OF_ID = {:#x}', + crc32(tlobject.result.encode('ascii'))) builder.writeln() # Flag arguments must go last @@ -265,9 +260,7 @@ class TLGenerator: # Write the __init__ function if args: - builder.writeln( - 'def __init__(self, {}):'.format(', '.join(args)) - ) + builder.writeln('def __init__(self, {}):', ', '.join(args)) else: builder.writeln('def __init__(self):') @@ -286,30 +279,27 @@ class TLGenerator: builder.writeln('"""') for arg in args: if not arg.flag_indicator: - builder.writeln(':param {} {}:'.format( - arg.doc_type_hint(), arg.name - )) + builder.writeln(':param {} {}:', + arg.doc_type_hint(), arg.name) builder.current_indent -= 1 # It will auto-indent (':') # We also want to know what type this request returns # or to which type this constructor belongs to builder.writeln() if tlobject.is_function: - builder.write(':returns {}: '.format(tlobject.result)) + builder.write(':returns {}: ', tlobject.result) else: - builder.write('Constructor for {}: '.format(tlobject.result)) + builder.write('Constructor for {}: ', tlobject.result) constructors = type_constructors[tlobject.result] if not constructors: builder.writeln('This type has no constructors.') elif len(constructors) == 1: - builder.writeln('Instance of {}.'.format( - constructors[0].class_name() - )) + builder.writeln('Instance of {}.', + constructors[0].class_name()) else: - builder.writeln('Instance of either {}.'.format( - ', '.join(c.class_name() for c in constructors) - )) + builder.writeln('Instance of either {}.', ', '.join( + c.class_name() for c in constructors)) builder.writeln('"""') @@ -327,8 +317,8 @@ class TLGenerator: for arg in args: if not arg.can_be_inferred: - builder.writeln('self.{0} = {0} # type: {1}'.format( - arg.name, arg.python_type_hint())) + builder.writeln('self.{0} = {0} # type: {1}', + arg.name, arg.python_type_hint()) continue # Currently the only argument that can be @@ -350,7 +340,7 @@ class TLGenerator: builder.writeln( "self.random_id = random_id if random_id " - "is not None else {}".format(code) + "is not None else {}", code ) else: raise ValueError('Cannot infer a value for ', arg) @@ -374,27 +364,27 @@ class TLGenerator: base_types = ('string', 'bytes', 'int', 'long', 'int128', 'int256', 'double', 'Bool', 'true', 'date') - builder.write("'_': '{}'".format(tlobject.class_name())) + builder.write("'_': '{}'", tlobject.class_name()) for arg in args: builder.writeln(',') - builder.write("'{}': ".format(arg.name)) + builder.write("'{}': ", arg.name) if arg.type in base_types: if arg.is_vector: - builder.write('[] if self.{0} is None else self.{0}[:]' - .format(arg.name)) + builder.write('[] if self.{0} is None else self.{0}[:]', + arg.name) else: - builder.write('self.{}'.format(arg.name)) + builder.write('self.{}', arg.name) else: if arg.is_vector: builder.write( '[] if self.{0} is None else [None ' - 'if x is None else x.to_dict() for x in self.{0}]' - .format(arg.name) + 'if x is None else x.to_dict() for x in self.{0}]', + arg.name ) else: builder.write( - 'None if self.{0} is None else self.{0}.to_dict()' - .format(arg.name) + 'None if self.{0} is None else self.{0}.to_dict()', + arg.name ) builder.writeln() @@ -421,17 +411,16 @@ class TLGenerator: .format(a.name) for a in ra) builder.writeln( "assert ({}) or ({}), '{} parameters must all " - "be False-y (like None) or all me True-y'".format( - ' and '.join(cnd1), ' and '.join(cnd2), - ', '.join(a.name for a in ra) - ) + "be False-y (like None) or all me True-y'", + ' and '.join(cnd1), ' and '.join(cnd2), + ', '.join(a.name for a in ra) ) builder.writeln("return b''.join((") builder.current_indent += 1 # First constructor code, we already know its bytes - builder.writeln('{},'.format(repr(struct.pack('3.5 feature, so add another join. @@ -560,7 +551,7 @@ class TLGenerator: arg.is_vector = True arg.is_flag = old_flag - builder.write(' for x in {})'.format(name)) + builder.write(' for x in {})', name) elif arg.flag_indicator: # Calculate the flags with those items which are not None @@ -579,41 +570,39 @@ class TLGenerator: elif 'int' == arg.type: # struct.pack is around 4 times faster than int.to_bytes - builder.write("struct.pack('