Merge branch 'master' into asyncio

This commit is contained in:
Lonami Exo 2018-04-01 17:08:50 +02:00
commit 1eb418e1ab
21 changed files with 740 additions and 391 deletions

View File

@ -42,6 +42,9 @@ extensions = [
'custom_roles' '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. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ['_templates']
@ -157,7 +160,7 @@ latex_elements = {
# author, documentclass [howto, manual, or own class]). # author, documentclass [howto, manual, or own class]).
latex_documents = [ latex_documents = [
(master_doc, 'Telethon.tex', 'Telethon Documentation', (master_doc, 'Telethon.tex', 'Telethon Documentation',
'Jeff', 'manual'), author, 'manual'),
] ]

View File

@ -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). 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, 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 .. code-block:: python
import os from telethon.errors import EmailUnconfirmedError
from hashlib import sha256
from telethon.tl.functions import account # Sets 2FA password for first time:
from telethon.tl.types.account import PasswordInputSettings await client.edit_2fa(new_password='supersecurepassword')
new_salt = client(account.GetPasswordRequest()).new_salt # Changes password:
salt = new_salt + os.urandom(8) # new random salt await client.edit_2fa(current_password='supersecurepassword',
new_password='changedmymind')
pw = 'secret'.encode('utf-8') # type your new password here
hint = 'hint' # Clears current password (i.e. removes 2FA):
await client.edit_2fa(current_password='changedmymind', new_password=None)
pw_salted = salt + pw + salt
pw_hash = sha256(pw_salted).digest() # Sets new password with recovery email:
try:
result = await client(account.UpdatePasswordSettingsRequest( await client.edit_2fa(new_password='memes and dreams',
current_password_hash=salt, email='JohnSmith@example.com')
new_settings=PasswordInputSettings( # Raises error (you need to check your email to complete 2FA setup.)
new_salt=salt, except EmailUnconfirmedError:
new_password_hash=pw_hash, # You can put email checking code here if desired.
hint=hint pass
)
)) # Also take note that unless you remove 2FA or explicitly
# give email parameter again it will keep the last used setting
Thanks to `Issue 259 <https://github.com/LonamiWebs/Telethon/issues/259>`_
for the tip! # 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#installation
__ https://github.com/Anorov/PySocks#usage-1 __ https://github.com/Anorov/PySocks#usage-1

View File

@ -32,7 +32,7 @@ you're able to just do this:
# Dialogs are the "conversations you have open". # Dialogs are the "conversations you have open".
# This method returns a list of Dialog, which # This method returns a list of Dialog, which
# has the .entity attribute and other information. # 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. # All of these work and do the same.
lonami = await client.get_entity('lonami') lonami = await client.get_entity('lonami')
@ -44,27 +44,17 @@ you're able to just do this:
contact = await client.get_entity('+34xxxxxxxxx') contact = await client.get_entity('+34xxxxxxxxx')
friend = await client.get_entity(friend_id) friend = await client.get_entity(friend_id)
# Using Peer/InputPeer (note that the API may return these) # Getting entities through their ID (User, Chat or Channel)
# users, chats and channels may all have the same ID, so it's entity = await client.get_entity(some_id)
# necessary to wrap (at least) chat and channels inside Peer.
# # You can be more explicit about the type for said ID by wrapping
# NOTICE how the IDs *must* be wrapped inside a Peer() so the # it inside a Peer instance. This is recommended but not necessary.
# library knows their type.
from telethon.tl.types import PeerUser, PeerChat, PeerChannel from telethon.tl.types import PeerUser, PeerChat, PeerChannel
my_user = await client.get_entity(PeerUser(some_id)) my_user = await client.get_entity(PeerUser(some_id))
my_chat = await client.get_entity(PeerChat(some_id)) my_chat = await client.get_entity(PeerChat(some_id))
my_channel = await client.get_entity(PeerChannel(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 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. 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!')`` That way, convenience calls such as ``client.send_message('lonami', 'hi!')``

View File

@ -14,6 +14,36 @@ it can take advantage of new goodies!
.. contents:: List of All Versions .. 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) Iterator methods (v0.18.1)
========================== ==========================

View File

@ -11,10 +11,9 @@ Working with Chats and Channels
Joining a chat or channel Joining a chat or channel
************************* *************************
Note that `Chat`__\ s are normal groups, and `Channel`__\ s are a Note that :tl:`Chat` are normal groups, and :tl:`Channel` are a
special form of `Chat`__\ s, special form of ``Chat``, which can also be super-groups if
which can also be super-groups if their ``megagroup`` member is their ``megagroup`` member is ``True``.
``True``.
Joining a public channel 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) Retrieving all chat members (channels too)
****************************************** ******************************************
You can use
`client.get_participants <telethon.telegram_client.TelegramClient.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 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 to use `GetParticipantsRequest`__. As we can see it needs an
`InputChannel`__, (passing the mega-group or channel you're going to `InputChannel`__, (passing the mega-group or channel you're going to
@ -134,9 +140,10 @@ a fixed limit:
.. note:: .. note::
It is **not** possible to get more than 10,000 members from a If you need more than 10,000 members from a group you should use the
group. It's a hard limit impossed by Telegram and there is mentioned ``client.get_participants(..., aggressive=True)``. It will
nothing you can do about it. Refer to `issue 573`__ for more. 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`__, 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/methods/channels/get_participants.html
__ https://lonamiwebs.github.io/Telethon/types/channel_participants_filter.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/channel_participants_search.html
__ https://lonamiwebs.github.io/Telethon/constructors/channels/channel_participants.html
__ https://github.com/LonamiWebs/Telethon/issues/573 __ https://github.com/LonamiWebs/Telethon/issues/573
__ https://lonamiwebs.github.io/Telethon/constructors/channels/channel_participants.html
Recent Actions Recent Actions

View File

@ -11,18 +11,27 @@ Working with messages
Forwarding messages Forwarding messages
******************* *******************
Note that ForwardMessageRequest_ (note it's Message, singular) will *not* This request is available as a friendly method through
work if channels are involved. This is because channel (and megagroups) IDs `client.forward_messages <telethon.telegram_client.TelegramClient.forward_messages>`,
are not unique, so you also need to know who the sender is (a parameter this and can be used like shown below:
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:
.. code-block:: python .. 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 from telethon.tl.functions.messages import ForwardMessagesRequest
# note the s ^
messages = foo() # retrieve a few messages (or even one, in a list) messages = foo() # retrieve a few messages (or even one, in a list)
from_entity = bar() 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 .. _ForwardMessagesRequest: https://lonamiwebs.github.io/Telethon/methods/messages/forward_messages.html
.. _SearchRequest: https://lonamiwebs.github.io/Telethon/methods/messages/search.html .. _SearchRequest: https://lonamiwebs.github.io/Telethon/methods/messages/search.html
.. _issues: https://github.com/LonamiWebs/Telethon/issues/215 .. _issues: https://github.com/LonamiWebs/Telethon/issues/215

View File

@ -78,6 +78,9 @@ def rpc_message_to_error(code, message, report_method=None):
if code == 404: if code == 404:
return NotFoundError(message) return NotFoundError(message)
if code == 406:
return AuthKeyError(message)
if code == 500: if code == 500:
return ServerError(message) return ServerError(message)

View File

@ -56,6 +56,19 @@ class NotFoundError(RPCError):
self.message = message 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): class FloodError(RPCError):
""" """
The maximum allowed number of attempts to invoke the given method The maximum allowed number of attempts to invoke the given method

View File

@ -46,11 +46,11 @@ class _EventBuilder(abc.ABC):
The common event builder, with builtin support to filter per chat. The common event builder, with builtin support to filter per chat.
Args: Args:
chats (:obj:`entity`, optional): chats (`entity`, optional):
May be one or more entities (username/peer/etc.). By default, May be one or more entities (username/peer/etc.). By default,
only matching chats will be handled. 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 Whether to treat the chats as a blacklist instead of
as a whitelist (default). This means that every chat as a whitelist (default). This means that every chat
will be handled *except* those specified in ``chats`` will be handled *except* those specified in ``chats``
@ -118,11 +118,15 @@ class _EventCommon(abc.ABC):
try: try:
if isinstance(chat, types.InputPeerChannel): if isinstance(chat, types.InputPeerChannel):
result = await self._client( result = await self._client(
functions.channels.GetMessagesRequest(chat, [msg_id]) functions.channels.GetMessagesRequest(chat, [
types.InputMessageID(msg_id)
])
) )
else: else:
result = await self._client( result = await self._client(
functions.messages.GetMessagesRequest([msg_id]) functions.messages.GetMessagesRequest([
types.InputMessageID(msg_id)
])
) )
except RPCError: except RPCError:
return None, None return None, None
@ -228,15 +232,15 @@ class NewMessage(_EventBuilder):
Represents a new message event builder. Represents a new message event builder.
Args: Args:
incoming (:obj:`bool`, optional): incoming (`bool`, optional):
If set to ``True``, only **incoming** messages will be handled. If set to ``True``, only **incoming** messages will be handled.
Mutually exclusive with ``outgoing`` (can only set one of either). 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. If set to ``True``, only **outgoing** messages will be handled.
Mutually exclusive with ``incoming`` (can only set one of either). 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. If set, only messages matching this pattern will be handled.
You can specify a regex-like string which will be matched You can specify a regex-like string which will be matched
against the message, a callable function that returns ``True`` against the message, a callable function that returns ``True``
@ -300,7 +304,7 @@ class NewMessage(_EventBuilder):
else: else:
return return
event._entities = update.entities event._entities = update._entities
return self._message_filter_event(event) return self._message_filter_event(event)
def _message_filter_event(self, event): def _message_filter_event(self, event):
@ -330,16 +334,16 @@ class NewMessage(_EventBuilder):
message (:tl:`Message`): message (:tl:`Message`):
This is the original :tl:`Message` object. This is the original :tl:`Message` object.
is_private (:obj:`bool`): is_private (`bool`):
True if the message was sent as a private message. 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. 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. 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. Whether the message is a reply to some other or not.
""" """
def __init__(self, message): def __init__(self, message):
@ -501,11 +505,13 @@ class NewMessage(_EventBuilder):
if self._reply_message is None: if self._reply_message is None:
if isinstance(await self.input_chat, types.InputPeerChannel): if isinstance(await self.input_chat, types.InputPeerChannel):
r = await self._client(functions.channels.GetMessagesRequest( 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: else:
r = await self._client(functions.messages.GetMessagesRequest( 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): if not isinstance(r, types.messages.MessagesNotModified):
self._reply_message = r.messages[0] self._reply_message = r.messages[0]
@ -692,7 +698,7 @@ class ChatAction(_EventBuilder):
else: else:
return return
event._entities = update.entities event._entities = update._entities
return self._filter_event(event) return self._filter_event(event)
class Event(_EventCommon): class Event(_EventCommon):
@ -700,35 +706,35 @@ class ChatAction(_EventBuilder):
Represents the event of a new chat action. Represents the event of a new chat action.
Members: Members:
new_pin (:obj:`bool`): new_pin (`bool`):
``True`` if there is a new pin. ``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). ``True`` if there's a new chat photo (or it was removed).
photo (:tl:`Photo`, optional): photo (:tl:`Photo`, optional):
The new photo (or ``None`` if it was removed). 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. ``True`` if the user was added by some other.
user_joined (:obj:`bool`): user_joined (`bool`):
``True`` if the user joined on their own. ``True`` if the user joined on their own.
user_left (:obj:`bool`): user_left (`bool`):
``True`` if the user left on their own. ``True`` if the user left on their own.
user_kicked (:obj:`bool`): user_kicked (`bool`):
``True`` if the user was kicked by some other. ``True`` if the user was kicked by some other.
created (:obj:`bool`, optional): created (`bool`, optional):
``True`` if this chat was just created. ``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. The new title string for the chat, if applicable.
unpin (:obj:`bool`): unpin (`bool`):
``True`` if the existing pin gets unpinned. ``True`` if the existing pin gets unpinned.
""" """
def __init__(self, where, new_pin=None, new_photo=None, 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: if isinstance(self._pinned_message, int) and await self.input_chat:
r = await self._client(functions.channels.GetMessagesRequest( r = await self._client(functions.channels.GetMessagesRequest(
self._input_chat, [self._pinned_message] self._input_chat, [
types.InputMessageID(self._pinned_message)
]
)) ))
try: try:
self._pinned_message = next( self._pinned_message = next(
@ -941,7 +949,7 @@ class UserUpdate(_EventBuilder):
else: else:
return return
event._entities = update.entities event._entities = update._entities
return self._filter_event(event) return self._filter_event(event)
class Event(_EventCommon): class Event(_EventCommon):
@ -949,62 +957,62 @@ class UserUpdate(_EventBuilder):
Represents the event of an user status update (last seen, joined). Represents the event of an user status update (last seen, joined).
Members: Members:
online (:obj:`bool`, optional): online (`bool`, optional):
``True`` if the user is currently online, ``False`` otherwise. ``True`` if the user is currently online, ``False`` otherwise.
Might be ``None`` if this information is not present. 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. Exact date when the user was last seen if known.
until (:obj:`datetime`, optional): until (`datetime`, optional):
Until when will the user remain online. Until when will the user remain online.
within_months (:obj:`bool`): within_months (`bool`):
``True`` if the user was seen within 30 days. ``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. ``True`` if the user was seen within 7 days.
recently (:obj:`bool`): recently (`bool`):
``True`` if the user was seen within a day. ``True`` if the user was seen within a day.
action (:tl:`SendMessageAction`, optional): action (:tl:`SendMessageAction`, optional):
The "typing" action if any the user is performing if any. The "typing" action if any the user is performing if any.
cancel (:obj:`bool`): cancel (`bool`):
``True`` if the action was cancelling other actions. ``True`` if the action was cancelling other actions.
typing (:obj:`bool`): typing (`bool`):
``True`` if the action is typing a message. ``True`` if the action is typing a message.
recording (:obj:`bool`): recording (`bool`):
``True`` if the action is recording something. ``True`` if the action is recording something.
uploading (:obj:`bool`): uploading (`bool`):
``True`` if the action is uploading something. ``True`` if the action is uploading something.
playing (:obj:`bool`): playing (`bool`):
``True`` if the action is playing a game. ``True`` if the action is playing a game.
audio (:obj:`bool`): audio (`bool`):
``True`` if what's being recorded/uploaded is an audio. ``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. ``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. ``True`` if what's being recorded/uploaded is an video.
document (:obj:`bool`): document (`bool`):
``True`` if what's being uploaded is document. ``True`` if what's being uploaded is document.
geo (:obj:`bool`): geo (`bool`):
``True`` if what's being uploaded is a geo. ``True`` if what's being uploaded is a geo.
photo (:obj:`bool`): photo (`bool`):
``True`` if what's being uploaded is a photo. ``True`` if what's being uploaded is a photo.
contact (:obj:`bool`): contact (`bool`):
``True`` if what's being uploaded (selected) is a contact. ``True`` if what's being uploaded (selected) is a contact.
""" """
def __init__(self, user_id, status=None, typing=None): def __init__(self, user_id, status=None, typing=None):
@ -1090,7 +1098,7 @@ class MessageEdited(NewMessage):
else: else:
return return
event._entities = update.entities event._entities = update._entities
return self._message_filter_event(event) return self._message_filter_event(event)
class Event(NewMessage.Event): class Event(NewMessage.Event):
@ -1116,7 +1124,7 @@ class MessageDeleted(_EventBuilder):
else: else:
return return
event._entities = update.entities event._entities = update._entities
return self._filter_event(event) return self._filter_event(event)
class Event(_EventCommon): class Event(_EventCommon):
@ -1128,6 +1136,140 @@ class MessageDeleted(_EventBuilder):
self.deleted_ids = deleted_ids 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): class StopPropagation(Exception):
""" """
If this exception is raised in any of the handlers for a given event, If this exception is raised in any of the handlers for a given event,

View File

@ -479,11 +479,13 @@ class MtProtoSender:
reader.read_int(signed=False) # code reader.read_int(signed=False) # code
request_id = reader.read_long() request_id = reader.read_long()
inner_code = reader.read_int(signed=False) inner_code = reader.read_int(signed=False)
reader.seek(-4)
__log__.debug('Received response for request with ID %d', request_id) __log__.debug('Received response for request with ID %d', request_id)
request = self._pop_request(request_id) request = self._pop_request(request_id)
if inner_code == 0x2144ca19: # RPC Error if inner_code == 0x2144ca19: # RPC Error
reader.seek(4)
if self.session.report_errors and request: if self.session.report_errors and request:
error = rpc_message_to_error( error = rpc_message_to_error(
reader.read_int(), reader.tgread_string(), reader.read_int(), reader.tgread_string(),
@ -505,12 +507,10 @@ class MtProtoSender:
return True # All contents were read okay return True # All contents were read okay
elif request: elif request:
if inner_code == 0x3072cfa1: # GZip packed if inner_code == GzipPacked.CONSTRUCTOR_ID:
unpacked_data = gzip.decompress(reader.tgread_bytes()) with BinaryReader(GzipPacked.read(reader)) as compressed_reader:
with BinaryReader(unpacked_data) as compressed_reader:
request.on_response(compressed_reader) request.on_response(compressed_reader)
else: else:
reader.seek(-4)
request.on_response(reader) request.on_response(reader)
self.session.process_entities(request.result) self.session.process_entities(request.result)
@ -525,10 +525,17 @@ class MtProtoSender:
# session, it will be skipped by the handle_container(). # session, it will be skipped by the handle_container().
# For some reason this also seems to happen when downloading # For some reason this also seems to happen when downloading
# photos, where the server responds with FileJpeg(). # photos, where the server responds with FileJpeg().
try: def _try_read(r):
obj = reader.tgread_object() try:
except Exception as e: return r.tgread_object()
obj = '(failed to read: %s)' % e 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( __log__.warning(
'Lost request (ID %d) with code %s will be skipped, contents: %s', 'Lost request (ID %d) with code %s will be skipped, contents: %s',

View File

@ -9,7 +9,7 @@ from .crypto import rsa
from .errors import ( from .errors import (
RPCError, BrokenAuthKeyError, ServerError, FloodWaitError, RPCError, BrokenAuthKeyError, ServerError, FloodWaitError,
FloodTestPhoneWaitError, TypeNotFoundError, UnauthorizedError, FloodTestPhoneWaitError, TypeNotFoundError, UnauthorizedError,
PhoneMigrateError, NetworkMigrateError, UserMigrateError PhoneMigrateError, NetworkMigrateError, UserMigrateError, AuthKeyError
) )
from .network import authenticator, MtProtoSender, Connection, ConnectionMode from .network import authenticator, MtProtoSender, Connection, ConnectionMode
from .sessions import Session, SQLiteSession from .sessions import Session, SQLiteSession
@ -217,6 +217,15 @@ class TelegramBareClient:
self.disconnect() self.disconnect()
return await self.connect(_sync_updates=_sync_updates) 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: except (RPCError, ConnectionError) as e:
# Probably errors from the previous session, ignore them # Probably errors from the previous session, ignore them
__log__.error('Connection failed due to %s', e) __log__.error('Connection failed due to %s', e)

View File

@ -38,12 +38,12 @@ from .errors import (
RPCError, UnauthorizedError, PhoneCodeEmptyError, PhoneCodeExpiredError, RPCError, UnauthorizedError, PhoneCodeEmptyError, PhoneCodeExpiredError,
PhoneCodeHashEmptyError, PhoneCodeInvalidError, LocationInvalidError, PhoneCodeHashEmptyError, PhoneCodeInvalidError, LocationInvalidError,
SessionPasswordNeededError, FileMigrateError, PhoneNumberUnoccupiedError, SessionPasswordNeededError, FileMigrateError, PhoneNumberUnoccupiedError,
PhoneNumberOccupiedError PhoneNumberOccupiedError, EmailUnconfirmedError, PasswordEmptyError
) )
from .network import ConnectionMode from .network import ConnectionMode
from .tl.custom import Draft, Dialog from .tl.custom import Draft, Dialog
from .tl.functions.account import ( from .tl.functions.account import (
GetPasswordRequest GetPasswordRequest, UpdatePasswordSettingsRequest
) )
from .tl.functions.auth import ( from .tl.functions.auth import (
CheckPasswordRequest, LogOutRequest, SendCodeRequest, SignInRequest, CheckPasswordRequest, LogOutRequest, SendCodeRequest, SignInRequest,
@ -83,9 +83,10 @@ from .tl.types import (
InputMessageEntityMentionName, DocumentAttributeVideo, InputMessageEntityMentionName, DocumentAttributeVideo,
UpdateEditMessage, UpdateEditChannelMessage, UpdateShort, Updates, UpdateEditMessage, UpdateEditChannelMessage, UpdateShort, Updates,
MessageMediaWebPage, ChannelParticipantsSearch, PhotoSize, PhotoCachedSize, MessageMediaWebPage, ChannelParticipantsSearch, PhotoSize, PhotoCachedSize,
PhotoSizeEmpty, MessageService PhotoSizeEmpty, MessageService, ChatParticipants
) )
from .tl.types.messages import DialogsSlice from .tl.types.messages import DialogsSlice
from .tl.types.account import PasswordInputSettings, NoPassword
from .extensions import markdown, html from .extensions import markdown, html
__log__ = logging.getLogger(__name__) __log__ = logging.getLogger(__name__)
@ -96,52 +97,51 @@ class TelegramClient(TelegramBareClient):
Initializes the Telegram client with the specified API ID and Hash. Initializes the Telegram client with the specified API ID and Hash.
Args: Args:
session (:obj:`str` | :obj:`telethon.sessions.abstract.Session`, \ session (`str` | `telethon.sessions.abstract.Session`, `None`):
:obj:`None`):
The file name of the session file to be used if a string is 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 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, used otherwise. If it's ``None``, the session will not be saved,
and you should call :meth:`.log_out()` when you're done. 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. 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. 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 The connection mode to be used when creating a new connection
to the servers. Defaults to the ``TCP_FULL`` mode. to the servers. Defaults to the ``TCP_FULL`` mode.
This will only affect how messages are sent over the network This will only affect how messages are sent over the network
and how much processing is required before sending them. 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. Whether to connect to the servers through IPv6 or not.
By default this is ``False`` as IPv6 support is not By default this is ``False`` as IPv6 support is not
too widespread yet. too widespread yet.
proxy (:obj:`tuple` | :obj:`dict`, optional): proxy (`tuple` | `dict`, optional):
A tuple consisting of ``(socks.SOCKS5, 'host', port)``. A tuple consisting of ``(socks.SOCKS5, 'host', port)``.
See https://github.com/Anorov/PySocks#usage-1 for more. 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 If specified, represents how many extra threads should
be spawned to handle incoming updates, and updates will be spawned to handle incoming updates, and updates will
be kept in memory until they are processed. Note that be kept in memory until they are processed. Note that
you must set this to at least ``0`` if you want to be you must set this to at least ``0`` if you want to be
able to process updates through :meth:`updates.poll()`. 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 timeout to be used when receiving responses from
the network. Defaults to 5 seconds. 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 Whether to use an extra background thread or not. Defaults
to ``True`` so receiving items from the network happens to ``True`` so receiving items from the network happens
instantly, as soon as they arrive. Can still be disabled instantly, as soon as they arrive. Can still be disabled
if you want to run the library without any additional thread. 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``, Whether to report RPC errors or not. Defaults to ``True``,
see :ref:`api-status` for more information. see :ref:`api-status` for more information.
@ -204,10 +204,10 @@ class TelegramClient(TelegramBareClient):
Sends a code request to the specified phone number. Sends a code request to the specified phone number.
Args: Args:
phone (:obj:`str` | :obj:`int`): phone (`str` | `int`):
The phone to which the code will be sent. The phone to which the code will be sent.
force_sms (:obj:`bool`, optional): force_sms (`bool`, optional):
Whether to force sending as SMS. Whether to force sending as SMS.
Returns: Returns:
@ -247,36 +247,36 @@ class TelegramClient(TelegramBareClient):
(You are now logged in) (You are now logged in)
Args: Args:
phone (:obj:`str` | :obj:`int` | :obj:`callable`): phone (`str` | `int` | `callable`):
The phone (or callable without arguments to get it) The phone (or callable without arguments to get it)
to which the code will be sent. to which the code will be sent.
password (:obj:`callable`, optional): password (`callable`, optional):
The password for 2 Factor Authentication (2FA). The password for 2 Factor Authentication (2FA).
This is only required if it is enabled in your account. This is only required if it is enabled in your account.
bot_token (:obj:`str`): bot_token (`str`):
Bot Token obtained by `@BotFather <https://t.me/BotFather>`_ Bot Token obtained by `@BotFather <https://t.me/BotFather>`_
to log in as a bot. Cannot be specified with ``phone`` (only to log in as a bot. Cannot be specified with ``phone`` (only
one of either allowed). one of either allowed).
force_sms (:obj:`bool`, optional): force_sms (`bool`, optional):
Whether to force sending the code request as SMS. Whether to force sending the code request as SMS.
This only makes sense when signing in with a `phone`. 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 A callable that will be used to retrieve the Telegram
login code. Defaults to `input()`. 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 The first name to be used if signing up. This has no
effect if the account already exists and you sign in. 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. Similar to the first name, but for the last. Optional.
Returns: Returns:
This :obj:`TelegramClient`, so initialization This `TelegramClient`, so initialization
can be chained with ``.start()``. can be chained with ``.start()``.
""" """
@ -367,26 +367,26 @@ class TelegramClient(TelegramBareClient):
or code that Telegram sent. or code that Telegram sent.
Args: Args:
phone (:obj:`str` | :obj:`int`): phone (`str` | `int`):
The phone to send the code to if no code was provided, The phone to send the code to if no code was provided,
or to override the phone that was previously used with or to override the phone that was previously used with
these requests. these requests.
code (:obj:`str` | :obj:`int`): code (`str` | `int`):
The code that Telegram sent. Note that if you have sent this The code that Telegram sent. Note that if you have sent this
code through the application itself it will immediately code through the application itself it will immediately
expire. If you want to send the code, obfuscate it somehow. expire. If you want to send the code, obfuscate it somehow.
If you're not doing any of this you can ignore this note. 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 2FA password, should be used if a previous call raised
SessionPasswordNeededError. SessionPasswordNeededError.
bot_token (:obj:`str`): bot_token (`str`):
Used to sign in as a bot. Not all requests will be available. Used to sign in as a bot. Not all requests will be available.
This should be the hash the @BotFather gave you. 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 The hash returned by .send_code_request. This can be set to None
to use the last hash known. to use the last hash known.
@ -443,13 +443,13 @@ class TelegramClient(TelegramBareClient):
You must call .send_code_request(phone) first. You must call .send_code_request(phone) first.
Args: Args:
code (:obj:`str` | :obj:`int`): code (`str` | `int`):
The code sent by Telegram The code sent by Telegram
first_name (:obj:`str`): first_name (`str`):
The first name to be used by the new account. The first name to be used by the new account.
last_name (:obj:`str`, optional) last_name (`str`, optional)
Optional last name. Optional last name.
Returns: Returns:
@ -495,7 +495,7 @@ class TelegramClient(TelegramBareClient):
or None if the request fails (hence, not authenticated). or None if the request fails (hence, not authenticated).
Args: Args:
input_peer (:obj:`bool`, optional): input_peer (`bool`, optional):
Whether to return the :tl:`InputPeerUser` version or the normal Whether to return the :tl:`InputPeerUser` version or the normal
:tl:`User`. This can be useful if you just need to know the ID :tl:`User`. This can be useful if you just need to know the ID
of yourself. of yourself.
@ -527,27 +527,27 @@ class TelegramClient(TelegramBareClient):
Dialogs are the open "chats" or conversations with other people. Dialogs are the open "chats" or conversations with other people.
Args: Args:
limit (:obj:`int` | :obj:`None`): limit (`int` | `None`):
How many dialogs to be retrieved as maximum. Can be set to How many dialogs to be retrieved as maximum. Can be set to
``None`` to retrieve all dialogs. Note that this may take ``None`` to retrieve all dialogs. Note that this may take
whole minutes if you have hundreds of dialogs, as Telegram whole minutes if you have hundreds of dialogs, as Telegram
will tell the library to slow down through a will tell the library to slow down through a
``FloodWaitError``. ``FloodWaitError``.
offset_date (:obj:`datetime`, optional): offset_date (`datetime`, optional):
The offset date to be used. The offset date to be used.
offset_id (:obj:`int`, optional): offset_id (`int`, optional):
The message ID to be used as an offset. The message ID to be used as an offset.
offset_peer (:tl:`InputPeer`, optional): offset_peer (:tl:`InputPeer`, optional):
The peer to be used as an offset. 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. A single-item list to pass the total parameter by reference.
Yields: 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) limit = float('inf') if limit is None else int(limit)
if limit == 0: if limit == 0:
@ -617,9 +617,9 @@ class TelegramClient(TelegramBareClient):
""" """
Iterator over all open draft messages. Iterator over all open draft messages.
Instances of :obj:`telethon.tl.custom.draft.Draft` are yielded. Instances of `telethon.tl.custom.draft.Draft` are yielded.
You can call :obj:`telethon.tl.custom.draft.Draft.set_message` You can call `telethon.tl.custom.draft.Draft.set_message`
to change the message or :obj:`telethon.tl.custom.draft.Draft.delete` to change the message or `telethon.tl.custom.draft.Draft.delete`
among other things. among other things.
""" """
for update in (await self(GetAllDraftsRequest())).updates: 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). Sends the given message to the specified entity (user/chat/channel).
Args: Args:
entity (:obj:`entity`): entity (`entity`):
To who will it be sent. 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. 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, 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. 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), Can be 'md' or 'markdown' for markdown-like parsing (default),
or 'htm' or 'html' for HTML-like parsing. If ``None`` or any or 'htm' or 'html' for HTML-like parsing. If ``None`` or any
other false-y value is provided, the message will be sent with other false-y value is provided, the message will be sent with
no formatting. no formatting.
link_preview (:obj:`bool`, optional): link_preview (`bool`, optional):
Should the link preview be shown? Should the link preview be shown?
file (:obj:`file`, optional): file (`file`, optional):
Sends a message with a file attached (e.g. a photo, Sends a message with a file attached (e.g. a photo,
video, audio or document). The ``message`` may be empty. 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. 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. Whether the existing draft should be cleared or not.
Has no effect when sending a file. Has no effect when sending a file.
@ -805,13 +805,13 @@ class TelegramClient(TelegramBareClient):
Forwards the given message(s) to the specified entity. Forwards the given message(s) to the specified entity.
Args: Args:
entity (:obj:`entity`): entity (`entity`):
To which entity the message(s) will be forwarded. 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. 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 If the given messages are integer IDs and not instances
of the ``Message`` class, this *must* be specified in of the ``Message`` class, this *must* be specified in
order for the forward to work. 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). Edits the given message ID (to change its contents or disable preview).
Args: Args:
entity (:obj:`entity`): entity (`entity`):
From which chat to edit the message. 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. The ID of the message (or ``Message`` itself) to be edited.
message (:obj:`str`, optional): message (`str`, optional):
The new text of the message. 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), Can be 'md' or 'markdown' for markdown-like parsing (default),
or 'htm' or 'html' for HTML-like parsing. If ``None`` or any or 'htm' or 'html' for HTML-like parsing. If ``None`` or any
other false-y value is provided, the message will be sent with other false-y value is provided, the message will be sent with
no formatting. no formatting.
link_preview (:obj:`bool`, optional): link_preview (`bool`, optional):
Should the link preview be shown? Should the link preview be shown?
Raises: Raises:
@ -902,15 +902,15 @@ class TelegramClient(TelegramBareClient):
Deletes a message from a chat, optionally "for everyone". Deletes a message from a chat, optionally "for everyone".
Args: Args:
entity (:obj:`entity`): entity (`entity`):
From who the message will be deleted. This can actually From who the message will be deleted. This can actually
be ``None`` for normal chats, but **must** be present be ``None`` for normal chats, but **must** be present
for channels and megagroups. 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. 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. Whether the message should be deleted for everyone or not.
By default it has the opposite behaviour of official clients, By default it has the opposite behaviour of official clients,
and it will delete the message for everyone. and it will delete the message for everyone.
@ -944,48 +944,48 @@ class TelegramClient(TelegramBareClient):
Iterator over the message history for the specified entity. Iterator over the message history for the specified entity.
Args: Args:
entity (:obj:`entity`): entity (`entity`):
The entity from whom to retrieve the message history. 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 Number of messages to be retrieved. Due to limitations with
the API retrieving more than 3000 messages will take longer the API retrieving more than 3000 messages will take longer
than half a minute (or even more based on previous calls). than half a minute (or even more based on previous calls).
The limit may also be ``None``, which would eventually return The limit may also be ``None``, which would eventually return
the whole history. the whole history.
offset_date (:obj:`datetime`): offset_date (`datetime`):
Offset date (messages *previous* to this date will be Offset date (messages *previous* to this date will be
retrieved). Exclusive. retrieved). Exclusive.
offset_id (:obj:`int`): offset_id (`int`):
Offset message ID (only messages *previous* to the given Offset message ID (only messages *previous* to the given
ID will be retrieved). Exclusive. 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 All the messages with a higher (newer) ID or equal to this will
be excluded be excluded
min_id (:obj:`int`): min_id (`int`):
All the messages with a lower (older) ID or equal to this will All the messages with a lower (older) ID or equal to this will
be excluded. be excluded.
add_offset (:obj:`int`): add_offset (`int`):
Additional message offset (all of the specified offsets + Additional message offset (all of the specified offsets +
this offset = older messages). this offset = older messages).
batch_size (:obj:`int`): batch_size (`int`):
Messages will be returned in chunks of this size (100 is Messages will be returned in chunks of this size (100 is
the maximum). While it makes no sense to modify this value, the maximum). While it makes no sense to modify this value,
you are still free to do so. you are still free to do so.
wait_time (:obj:`int`): wait_time (`int`):
Wait time between different :tl:`GetHistoryRequest`. Use this Wait time between different :tl:`GetHistoryRequest`. Use this
parameter to avoid hitting the ``FloodWaitError`` as needed. parameter to avoid hitting the ``FloodWaitError`` as needed.
If left to ``None``, it will default to 1 second only if If left to ``None``, it will default to 1 second only if
the limit is higher than 3000. the limit is higher than 3000.
_total (:obj:`list`, optional): _total (`list`, optional):
A single-item list to pass the total parameter by reference. A single-item list to pass the total parameter by reference.
Yields: Yields:
@ -1103,17 +1103,17 @@ class TelegramClient(TelegramBareClient):
read their messages, also known as the "double check"). read their messages, also known as the "double check").
Args: Args:
entity (:obj:`entity`): entity (`entity`):
The chat where these messages are located. 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. Either a list of messages or a single message.
max_id (:obj:`int`): max_id (`int`):
Overrides messages, until which message should the Overrides messages, until which message should the
acknowledge should be sent. acknowledge should be sent.
clear_mentions (:obj:`bool`): clear_mentions (`bool`):
Whether the mention badge should be cleared (so that Whether the mention badge should be cleared (so that
there are no more mentions) or not for the given entity. 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. Iterator over the participants belonging to the specified chat.
Args: Args:
entity (:obj:`entity`): entity (`entity`):
The entity from which to retrieve the participants list. The entity from which to retrieve the participants list.
limit (:obj:`int`): limit (`int`):
Limits amount of participants fetched. Limits amount of participants fetched.
search (:obj:`str`, optional): search (`str`, optional):
Look for participants with this string in name/username. Look for participants with this string in name/username.
filter (:tl:`ChannelParticipantsFilter`, optional): filter (:tl:`ChannelParticipantsFilter`, optional):
@ -1182,7 +1182,7 @@ class TelegramClient(TelegramBareClient):
Note that you might not have permissions for some filter. Note that you might not have permissions for some filter.
This has no effect for normal chats or users. 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 Aggressively looks for all participants in the chat in
order to get more than 10,000 members (a hard limit order to get more than 10,000 members (a hard limit
imposed by Telegram). Note that this might take a long 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 This has no effect for groups or channels with less than
10,000 members, or if a ``filter`` is given. 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. A single-item list to pass the total parameter by reference.
Yields: Yields:
@ -1282,6 +1282,11 @@ class TelegramClient(TelegramBareClient):
elif isinstance(entity, InputPeerChat): elif isinstance(entity, InputPeerChat):
# TODO We *could* apply the `filter` here ourselves # TODO We *could* apply the `filter` here ourselves
full = await self(GetFullChatRequest(entity.chat_id)) 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: if _total:
_total[0] = len(full.full_chat.participants.participants) _total[0] = len(full.full_chat.participants.participants)
@ -1336,10 +1341,10 @@ class TelegramClient(TelegramBareClient):
Sends a file to the specified entity. Sends a file to the specified entity.
Args: Args:
entity (:obj:`entity`): entity (`entity`):
Who will receive the file. 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. 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 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 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 sent as an album in the order in which they appear, sliced
in chunks of 10 if more than 10 are given. in chunks of 10 if more than 10 are given.
caption (:obj:`str`, optional): caption (`str`, optional):
Optional caption for the sent media message. 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 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 the extension of an image file or a video file, it will be
sent as such. Otherwise always as a document. sent as such. Otherwise always as a document.
progress_callback (:obj:`callable`, optional): progress_callback (`callable`, optional):
A callback function accepting two parameters: A callback function accepting two parameters:
``(sent bytes, total)``. ``(sent bytes, total)``.
reply_to (:obj:`int` | :tl:`Message`): reply_to (`int` | :tl:`Message`):
Same as reply_to from .send_message(). Same as reply_to from .send_message().
attributes (:obj:`list`, optional): attributes (`list`, optional):
Optional attributes that override the inferred ones, like Optional attributes that override the inferred ones, like
:tl:`DocumentAttributeFilename` and so on. :tl:`DocumentAttributeFilename` and so on.
thumb (:obj:`str` | :obj:`bytes` | :obj:`file`, optional): thumb (`str` | `bytes` | `file`, optional):
Optional thumbnail (for videos). Optional thumbnail (for videos).
allow_cache (:obj:`bool`, optional): allow_cache (`bool`, optional):
Whether to allow using the cached version stored in the Whether to allow using the cached version stored in the
database or not. Defaults to ``True`` to avoid re-uploads. database or not. Defaults to ``True`` to avoid re-uploads.
Must be ``False`` if you wish to use different attributes Must be ``False`` if you wish to use different attributes
or thumb than those that were used when the file was cached. 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. The parse mode for the caption message.
Kwargs: Kwargs:
@ -1624,7 +1629,7 @@ class TelegramClient(TelegramBareClient):
will **not** upload the file to your own chat or any chat at all. will **not** upload the file to your own chat or any chat at all.
Args: 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. 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 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 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 Subsequent calls with the very same file will result in
immediate uploads, unless ``.clear_file_cache()`` is called. 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 Chunk size when uploading files. The larger, the less
requests will be made (up to 512KB maximum). 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. The file name which will be used on the resulting InputFile.
If not specified, the name will be taken from the ``file`` If not specified, the name will be taken from the ``file``
and if this is not a ``str``, it will be ``"unnamed"``. 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`` The type of cache to use (currently either ``InputDocument``
or ``InputPhoto``). If present and the file is small enough or ``InputPhoto``). If present and the file is small enough
to need the MD5, it will be checked against the database, to need the MD5, it will be checked against the database,
and if a match is found, the upload won't be made. Instead, and if a match is found, the upload won't be made. Instead,
an instance of type ``use_cache`` will be returned. an instance of type ``use_cache`` will be returned.
progress_callback (:obj:`callable`, optional): progress_callback (`callable`, optional):
A callback function accepting two parameters: A callback function accepting two parameters:
``(sent bytes, total)``. ``(sent bytes, total)``.
@ -1752,14 +1757,14 @@ class TelegramClient(TelegramBareClient):
Downloads the profile photo of the given entity (user/chat/channel). Downloads the profile photo of the given entity (user/chat/channel).
Args: Args:
entity (:obj:`entity`): entity (`entity`):
From who the photo will be downloaded. 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. The output file path, directory, or stream-like object.
If the path exists and is a file, it will be overwritten. 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. Whether to use the big version of the available photos.
Returns: Returns:
@ -1841,11 +1846,11 @@ class TelegramClient(TelegramBareClient):
message (:tl:`Message` | :tl:`Media`): message (:tl:`Message` | :tl:`Media`):
The media or message containing the media that will be downloaded. 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. The output file path, directory, or stream-like object.
If the path exists and is a file, it will be overwritten. 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: A callback function accepting two parameters:
``(received bytes, total)``. ``(received bytes, total)``.
@ -2066,19 +2071,19 @@ class TelegramClient(TelegramBareClient):
input_location (:tl:`InputFileLocation`): input_location (:tl:`InputFileLocation`):
The file location from which the file will be downloaded. 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. The output file path, directory, or stream-like object.
If the path exists and is a file, it will be overwritten. 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 Chunk size when downloading files. The larger, the less
requests will be made (up to 512KB maximum). 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. The file size that is about to be downloaded, if known.
Only used if ``progress_callback`` is specified. Only used if ``progress_callback`` is specified.
progress_callback (:obj:`callable`, optional): progress_callback (`callable`, optional):
A callback function accepting two parameters: A callback function accepting two parameters:
``(downloaded bytes, total)``. Note that the ``(downloaded bytes, total)``. Note that the
``total`` is the provided ``file_size``. ``total`` is the provided ``file_size``.
@ -2172,7 +2177,7 @@ class TelegramClient(TelegramBareClient):
Decorator helper method around add_event_handler(). Decorator helper method around add_event_handler().
Args: Args:
event (:obj:`_EventBuilder` | :obj:`type`): event (`_EventBuilder` | `type`):
The event builder class or instance to be used, The event builder class or instance to be used,
for instance ``events.NewMessage``. for instance ``events.NewMessage``.
""" """
@ -2208,10 +2213,10 @@ class TelegramClient(TelegramBareClient):
Registers the given callback to be called on the specified event. Registers the given callback to be called on the specified event.
Args: Args:
callback (:obj:`callable`): callback (`callable`):
The callable function accepting one parameter to be used. 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, The event builder class or instance to be used,
for instance ``events.NewMessage``. for instance ``events.NewMessage``.
@ -2286,7 +2291,7 @@ class TelegramClient(TelegramBareClient):
""" """
Turns the given entity into a valid Telegram user or chat. 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. The entity (or iterable of entities) to be transformed.
If it's a string which can be converted to an integer or starts 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. 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 use this kind of InputUser, InputChat and so on, so this is the
most suitable call to make for those cases. 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 The integer ID of an user or otherwise either of a
:tl:`PeerUser`, :tl:`PeerChat` or :tl:`PeerChannel`, for :tl:`PeerUser`, :tl:`PeerChat` or :tl:`PeerChannel`, for
which to get its ``Input*`` version. which to get its ``Input*`` version.
@ -2414,6 +2419,9 @@ class TelegramClient(TelegramBareClient):
Returns: Returns:
:tl:`InputPeerUser`, :tl:`InputPeerChat` or :tl:`InputPeerChannel`. :tl:`InputPeerUser`, :tl:`InputPeerChat` or :tl:`InputPeerChannel`.
""" """
if peer in ('me', 'self'):
return InputPeerSelf()
try: try:
# First try to get the entity from cache, otherwise figure it out # First try to get the entity from cache, otherwise figure it out
return self.session.get_input_entity(peer) return self.session.get_input_entity(peer)
@ -2421,8 +2429,6 @@ class TelegramClient(TelegramBareClient):
pass pass
if isinstance(peer, str): if isinstance(peer, str):
if peer in ('me', 'self'):
return InputPeerSelf()
return utils.get_input_peer(await self._get_entity_from_string(peer)) return utils.get_input_peer(await self._get_entity_from_string(peer))
original_peer = peer original_peer = peer
@ -2459,4 +2465,75 @@ class TelegramClient(TelegramBareClient):
'Make sure you have encountered this peer before.'.format(peer) '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 # endregion

View File

@ -1,4 +1,5 @@
from . import Draft from . import Draft
from .. import TLObject
from ... import utils from ... import utils
@ -13,7 +14,7 @@ class Dialog:
dialog (:tl:`Dialog`): dialog (:tl:`Dialog`):
The original ``Dialog`` instance. The original ``Dialog`` instance.
pinned (:obj:`bool`): pinned (`bool`):
Whether this dialog is pinned to the top or not. Whether this dialog is pinned to the top or not.
message (:tl:`Message`): message (:tl:`Message`):
@ -21,31 +22,31 @@ class Dialog:
will not be updated when new messages arrive, it's only set will not be updated when new messages arrive, it's only set
on creation of the instance. on creation of the instance.
date (:obj:`datetime`): date (`datetime`):
The date of the last message sent on this dialog. 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). The entity that belongs to this dialog (user, chat or channel).
input_entity (:tl:`InputPeer`): input_entity (:tl:`InputPeer`):
Input version of the entity. Input version of the entity.
id (:obj:`int`): id (`int`):
The marked ID of the entity, which is guaranteed to be unique. 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 Display name for this dialog. For chats and channels this is
their title, and for users it's "First-Name Last-Name". 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 How many messages are currently unread in this dialog. Note that
this value won't update when new messages arrive. 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 How many mentions are currently unread in this dialog. Note that
this value won't update when new messages arrive. 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``, The draft object in this dialog. It will not be ``None``,
so you can call ``draft.set_message(...)``. so you can call ``draft.set_message(...)``.
""" """
@ -73,3 +74,19 @@ class Dialog:
``client.send_message(dialog.input_entity, *args, **kwargs)``. ``client.send_message(dialog.input_entity, *args, **kwargs)``.
""" """
return await self._client.send_message(self.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)

View File

@ -1,7 +1,9 @@
import datetime import datetime
from .. import TLObject
from ..functions.messages import SaveDraftRequest from ..functions.messages import SaveDraftRequest
from ..types import UpdateDraftMessage, DraftMessage from ..types import UpdateDraftMessage, DraftMessage
from ...errors import RPCError
from ...extensions import markdown from ...extensions import markdown
@ -12,13 +14,13 @@ class Draft:
instances of this class when calling :meth:`get_drafts()`. instances of this class when calling :meth:`get_drafts()`.
Args: Args:
date (:obj:`datetime`): date (`datetime`):
The date of the draft. The date of the draft.
link_preview (:obj:`bool`): link_preview (`bool`):
Whether the link preview is enabled or not. 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. The message ID that the draft will reply to.
""" """
def __init__(self, client, peer, draft): def __init__(self, client, peer, draft):
@ -142,3 +144,24 @@ class Draft:
Deletes this draft, and returns ``True`` on success. Deletes this draft, and returns ``True`` on success.
""" """
return await self.set_message(text='') 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)

View File

@ -1,9 +1,9 @@
import logging
import pickle
import asyncio import asyncio
from collections import deque import itertools
import logging
from datetime import datetime from datetime import datetime
from . import utils
from .tl import types as tl from .tl import types as tl
__log__ = logging.getLogger(__name__) __log__ = logging.getLogger(__name__)
@ -42,14 +42,20 @@ class UpdateState:
# After running the script for over an hour and receiving over # After running the script for over an hour and receiving over
# 1000 updates, the only duplicates received were users going # 1000 updates, the only duplicates received were users going
# online or offline. We can trust the server until new reports. # online or offline. We can trust the server until new reports.
# This should only be used as read-only.
if isinstance(update, tl.UpdateShort): if isinstance(update, tl.UpdateShort):
update.update._entities = {}
self.handle_update(update.update) self.handle_update(update.update)
# Expand "Updates" into "Update", and pass these to callbacks. # Expand "Updates" into "Update", and pass these to callbacks.
# Since .users and .chats have already been processed, we # Since .users and .chats have already been processed, we
# don't need to care about those either. # don't need to care about those either.
elif isinstance(update, (tl.Updates, tl.UpdatesCombined)): 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: for u in update.updates:
u._entities = entities
self.handle_update(u) self.handle_update(u)
# TODO Handle "tl.UpdatesTooLong" # TODO Handle "tl.UpdatesTooLong"
else: else:
update._entities = {}
self.handle_update(update) self.handle_update(update)

View File

@ -261,12 +261,22 @@ def get_input_media(media, is_photo=False):
ttl_seconds=media.ttl_seconds ttl_seconds=media.ttl_seconds
) )
if isinstance(media, (Photo, photos.Photo, PhotoEmpty)):
return InputMediaPhoto(
id=get_input_photo(media)
)
if isinstance(media, MessageMediaDocument): if isinstance(media, MessageMediaDocument):
return InputMediaDocument( return InputMediaDocument(
id=get_input_document(media.document), id=get_input_document(media.document),
ttl_seconds=media.ttl_seconds ttl_seconds=media.ttl_seconds
) )
if isinstance(media, (Document, DocumentEmpty)):
return InputMediaDocument(
id=get_input_document(media)
)
if isinstance(media, FileLocation): if isinstance(media, FileLocation):
if is_photo: if is_photo:
return InputMediaUploadedPhoto(file=media) return InputMediaUploadedPhoto(file=media)

View File

@ -1,3 +1,3 @@
# Versions should comply with PEP440. # Versions should comply with PEP440.
# This line is parsed in setup.py: # This line is parsed in setup.py:
__version__ = '0.18.1' __version__ = '0.18.2'

View File

@ -11,6 +11,7 @@ known_base_classes = {
401: 'UnauthorizedError', 401: 'UnauthorizedError',
403: 'ForbiddenError', 403: 'ForbiddenError',
404: 'NotFoundError', 404: 'NotFoundError',
406: 'AuthKeyError',
420: 'FloodError', 420: 'FloodError',
500: 'ServerError', 500: 'ServerError',
} }

View File

@ -16,7 +16,7 @@ class SourceBuilder:
""" """
self.write(' ' * (self.current_indent * self.indent_size)) 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, """Writes a string into the source code,
applying indentation if required applying indentation if required
""" """
@ -26,13 +26,16 @@ class SourceBuilder:
if string.strip(): if string.strip():
self.indent() 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, """Writes a string into the source code _and_ appends a new line,
applying indentation if required applying indentation if required
""" """
self.write(string + '\n') self.write(string + '\n', *args, **kwargs)
self.on_new_line = True self.on_new_line = True
# If we're writing a block, increment indent for the next time # If we're writing a block, increment indent for the next time

View File

@ -556,7 +556,7 @@ accountDaysTTL#b8d0afdf days:int = AccountDaysTTL;
documentAttributeImageSize#6c37c15c w:int h:int = DocumentAttribute; documentAttributeImageSize#6c37c15c w:int h:int = DocumentAttribute;
documentAttributeAnimated#11b58939 = DocumentAttribute; documentAttributeAnimated#11b58939 = DocumentAttribute;
documentAttributeSticker#6319d612 flags:# mask:flags.1?true alt:string stickerset:InputStickerSet mask_coords:flags.0?MaskCoords = 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; 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; documentAttributeFilename#15590068 file_name:string = DocumentAttribute;
documentAttributeHasStickers#9801d2f7 = DocumentAttribute; documentAttributeHasStickers#9801d2f7 = DocumentAttribute;
@ -938,7 +938,15 @@ recentMeUrlStickerSet#bc0a57dc url:string set:StickerSetCovered = RecentMeUrl;
help.recentMeUrls#e0310d7 urls:Vector<RecentMeUrl> chats:Vector<Chat> users:Vector<User> = help.RecentMeUrls; help.recentMeUrls#e0310d7 urls:Vector<RecentMeUrl> chats:Vector<Chat> users:Vector<User> = help.RecentMeUrls;
inputSingleMedia#31bc3d25 media:InputMedia flags:# random_id:long message:string entities:flags.0?Vector<MessageEntity> = InputSingleMedia; inputSingleMedia#1cc6e91f flags:# media:InputMedia random_id:long message:string entities:flags.0?Vector<MessageEntity> = 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<WebAuthorization> users:Vector<User> = account.WebAuthorizations;
inputMessageID#a676a322 id:int = InputMessage;
inputMessageReplyTo#bad88395 id:int = InputMessage;
inputMessagePinned#86872538 = InputMessage;
---functions--- ---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.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.confirmPhone#5f2178c3 phone_code_hash:string phone_code:string = Bool;
account.getTmpPassword#4a82327e password_hash:bytes period:int = account.TmpPassword; 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<InputUser> = Vector<User>; users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;
users.getFullUser#ca30a5b1 id:InputUser = UserFull; 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.resetTopPeerRating#1ae373ac category:TopPeerCategory peer:InputPeer = Bool;
contacts.resetSaved#879537f1 = Bool; contacts.resetSaved#879537f1 = Bool;
messages.getMessages#4222fa74 id:Vector<int> = messages.Messages; messages.getMessages#63c66506 id:Vector<InputMessage> = 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.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.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; 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<int> = messages.AffectedMessages; channels.deleteMessages#84c1fd4e channel:InputChannel id:Vector<int> = messages.AffectedMessages;
channels.deleteUserHistory#d10dd71b channel:InputChannel user_id:InputUser = messages.AffectedHistory; channels.deleteUserHistory#d10dd71b channel:InputChannel user_id:InputUser = messages.AffectedHistory;
channels.reportSpam#fe087810 channel:InputChannel user_id:InputUser id:Vector<int> = Bool; channels.reportSpam#fe087810 channel:InputChannel user_id:InputUser id:Vector<int> = Bool;
channels.getMessages#93d7b347 channel:InputChannel id:Vector<int> = messages.Messages; channels.getMessages#ad8c9a23 channel:InputChannel id:Vector<InputMessage> = messages.Messages;
channels.getParticipants#123e05e9 channel:InputChannel filter:ChannelParticipantsFilter offset:int limit:int hash:int = channels.ChannelParticipants; 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.getParticipant#546dd7a6 channel:InputChannel user_id:InputUser = channels.ChannelParticipant;
channels.getChannels#a7f6bbb id:Vector<InputChannel> = messages.Chats; channels.getChannels#a7f6bbb id:Vector<InputChannel> = messages.Chats;

View File

@ -24,9 +24,11 @@ class TLGenerator:
self.output_dir = output_dir self.output_dir = output_dir
def _get_file(self, *paths): def _get_file(self, *paths):
"""Wrapper around ``os.path.join()`` with output as first path."""
return os.path.join(self.output_dir, *paths) return os.path.join(self.output_dir, *paths)
def _rm_if_exists(self, filename): def _rm_if_exists(self, filename):
"""Recursively deletes the given filename if it exists."""
file = self._get_file(filename) file = self._get_file(filename)
if os.path.exists(file): if os.path.exists(file):
if os.path.isdir(file): if os.path.isdir(file):
@ -35,19 +37,21 @@ class TLGenerator:
os.remove(file) os.remove(file)
def tlobjects_exist(self): 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')) return os.path.isfile(self._get_file('all_tlobjects.py'))
def clean_tlobjects(self): 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'): for name in ('functions', 'types', 'all_tlobjects.py'):
self._rm_if_exists(name) self._rm_if_exists(name)
def generate_tlobjects(self, scheme_file, import_depth): 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 # First ensure that the required parent directories exist
@ -85,42 +89,33 @@ class TLGenerator:
# Step 4: Once all the objects have been generated, # Step 4: Once all the objects have been generated,
# we can now group them in a single file # we can now group them in a single file
filename = os.path.join(self._get_file('all_tlobjects.py')) filename = os.path.join(self._get_file('all_tlobjects.py'))
with open(filename, 'w', encoding='utf-8') as file: with open(filename, 'w', encoding='utf-8') as file,\
with SourceBuilder(file) as builder: SourceBuilder(file) as builder:
builder.writeln(AUTO_GEN_NOTICE) builder.writeln(AUTO_GEN_NOTICE)
builder.writeln() builder.writeln()
builder.writeln('from . import types, functions') builder.writeln('from . import types, functions')
builder.writeln() builder.writeln()
# Create a constant variable to indicate which layer this is # Create a constant variable to indicate which layer this is
builder.writeln('LAYER = {}'.format( builder.writeln('LAYER = {}', TLParser.find_layer(scheme_file))
TLParser.find_layer(scheme_file)) builder.writeln()
)
builder.writeln()
# Then create the dictionary containing constructor_id: class # Then create the dictionary containing constructor_id: class
builder.writeln('tlobjects = {') builder.writeln('tlobjects = {')
builder.current_indent += 1 builder.current_indent += 1
# Fill the dictionary (0x1a2b3c4f: tl.full.type.path.Class) # Fill the dictionary (0x1a2b3c4f: tl.full.type.path.Class)
for tlobject in tlobjects: for tlobject in tlobjects:
constructor = hex(tlobject.id) builder.write('{:#010x}: ', tlobject.id)
if len(constructor) != 10: builder.write('functions' if tlobject.is_function else 'types')
# Make it a nice length 10 so it fits well if tlobject.namespace:
constructor = '0x' + constructor[2:].zfill(8) builder.write('.' + tlobject.namespace)
builder.write('{}: '.format(constructor)) builder.writeln('.{},', tlobject.class_name())
builder.write(
'functions' if tlobject.is_function else 'types')
if tlobject.namespace: builder.current_indent -= 1
builder.write('.' + tlobject.namespace) builder.writeln('}')
builder.writeln('.{},'.format(tlobject.class_name()))
builder.current_indent -= 1
builder.writeln('}')
@staticmethod @staticmethod
def _write_init_py(out_dir, depth, namespace_tlobjects, type_constructors): 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 # so they all can be serialized and sent, however, only the
# functions are "content_related". # functions are "content_related".
builder.writeln( 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, # Add the relative imports to the namespaces,
# unless we already are in a namespace. # unless we already are in a namespace.
if not ns: if not ns:
builder.writeln('from . import {}'.format(', '.join( builder.writeln('from . import {}', ', '.join(
x for x in namespace_tlobjects.keys() if x x for x in namespace_tlobjects.keys() if x
))) ))
# Import 'os' for those needing access to 'os.urandom()' # Import 'os' for those needing access to 'os.urandom()'
# Currently only 'random_id' needs 'os' to be imported, # Currently only 'random_id' needs 'os' to be imported,
@ -204,18 +200,18 @@ class TLGenerator:
if name == 'date': if name == 'date':
imports['datetime'] = ['datetime'] imports['datetime'] = ['datetime']
continue continue
elif not import_space in imports: elif import_space not in imports:
imports[import_space] = set() imports[import_space] = set()
imports[import_space].add('Type{}'.format(name)) imports[import_space].add('Type{}'.format(name))
# Add imports required for type checking. # Add imports required for type checking
builder.writeln('if TYPE_CHECKING:') if imports:
for namespace, names in imports.items(): builder.writeln('if TYPE_CHECKING:')
builder.writeln('from {} import {}'.format( for namespace, names in imports.items():
namespace, ', '.join(names))) builder.writeln('from {} import {}',
else: namespace, ', '.join(names))
builder.writeln('pass')
builder.end_block() builder.end_block()
# Generate the class for every TLObject # Generate the class for every TLObject
for t in tlobjects: for t in tlobjects:
@ -229,25 +225,24 @@ class TLGenerator:
for line in type_defs: for line in type_defs:
builder.writeln(line) builder.writeln(line)
@staticmethod @staticmethod
def _write_source_code(tlobject, builder, depth, type_constructors): 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 Additional information such as file path depth and
the Type: [Constructors] must be given for proper the ``Type: [Constructors]`` must be given for proper
importing and documentation strings. importing and documentation strings.
""" """
builder.writeln() builder.writeln()
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 # Class-level variable to store its Telegram's constructor ID
builder.writeln('CONSTRUCTOR_ID = {}'.format(hex(tlobject.id))) builder.writeln('CONSTRUCTOR_ID = {:#x}', tlobject.id)
builder.writeln('SUBCLASS_OF_ID = {}'.format( builder.writeln('SUBCLASS_OF_ID = {:#x}',
hex(crc32(tlobject.result.encode('ascii')))) crc32(tlobject.result.encode('ascii')))
)
builder.writeln() builder.writeln()
# Flag arguments must go last # Flag arguments must go last
@ -265,9 +260,7 @@ class TLGenerator:
# Write the __init__ function # Write the __init__ function
if args: if args:
builder.writeln( builder.writeln('def __init__(self, {}):', ', '.join(args))
'def __init__(self, {}):'.format(', '.join(args))
)
else: else:
builder.writeln('def __init__(self):') builder.writeln('def __init__(self):')
@ -286,30 +279,27 @@ class TLGenerator:
builder.writeln('"""') builder.writeln('"""')
for arg in args: for arg in args:
if not arg.flag_indicator: if not arg.flag_indicator:
builder.writeln(':param {} {}:'.format( builder.writeln(':param {} {}:',
arg.doc_type_hint(), arg.name arg.doc_type_hint(), arg.name)
))
builder.current_indent -= 1 # It will auto-indent (':') builder.current_indent -= 1 # It will auto-indent (':')
# We also want to know what type this request returns # We also want to know what type this request returns
# or to which type this constructor belongs to # or to which type this constructor belongs to
builder.writeln() builder.writeln()
if tlobject.is_function: if tlobject.is_function:
builder.write(':returns {}: '.format(tlobject.result)) builder.write(':returns {}: ', tlobject.result)
else: else:
builder.write('Constructor for {}: '.format(tlobject.result)) builder.write('Constructor for {}: ', tlobject.result)
constructors = type_constructors[tlobject.result] constructors = type_constructors[tlobject.result]
if not constructors: if not constructors:
builder.writeln('This type has no constructors.') builder.writeln('This type has no constructors.')
elif len(constructors) == 1: elif len(constructors) == 1:
builder.writeln('Instance of {}.'.format( builder.writeln('Instance of {}.',
constructors[0].class_name() constructors[0].class_name())
))
else: else:
builder.writeln('Instance of either {}.'.format( builder.writeln('Instance of either {}.', ', '.join(
', '.join(c.class_name() for c in constructors) c.class_name() for c in constructors))
))
builder.writeln('"""') builder.writeln('"""')
@ -327,8 +317,8 @@ class TLGenerator:
for arg in args: for arg in args:
if not arg.can_be_inferred: if not arg.can_be_inferred:
builder.writeln('self.{0} = {0} # type: {1}'.format( builder.writeln('self.{0} = {0} # type: {1}',
arg.name, arg.python_type_hint())) arg.name, arg.python_type_hint())
continue continue
# Currently the only argument that can be # Currently the only argument that can be
@ -350,7 +340,7 @@ class TLGenerator:
builder.writeln( builder.writeln(
"self.random_id = random_id if random_id " "self.random_id = random_id if random_id "
"is not None else {}".format(code) "is not None else {}", code
) )
else: else:
raise ValueError('Cannot infer a value for ', arg) raise ValueError('Cannot infer a value for ', arg)
@ -374,27 +364,27 @@ class TLGenerator:
base_types = ('string', 'bytes', 'int', 'long', 'int128', base_types = ('string', 'bytes', 'int', 'long', 'int128',
'int256', 'double', 'Bool', 'true', 'date') 'int256', 'double', 'Bool', 'true', 'date')
builder.write("'_': '{}'".format(tlobject.class_name())) builder.write("'_': '{}'", tlobject.class_name())
for arg in args: for arg in args:
builder.writeln(',') builder.writeln(',')
builder.write("'{}': ".format(arg.name)) builder.write("'{}': ", arg.name)
if arg.type in base_types: if arg.type in base_types:
if arg.is_vector: if arg.is_vector:
builder.write('[] if self.{0} is None else self.{0}[:]' builder.write('[] if self.{0} is None else self.{0}[:]',
.format(arg.name)) arg.name)
else: else:
builder.write('self.{}'.format(arg.name)) builder.write('self.{}', arg.name)
else: else:
if arg.is_vector: if arg.is_vector:
builder.write( builder.write(
'[] if self.{0} is None else [None ' '[] if self.{0} is None else [None '
'if x is None else x.to_dict() for x in self.{0}]' 'if x is None else x.to_dict() for x in self.{0}]',
.format(arg.name) arg.name
) )
else: else:
builder.write( builder.write(
'None if self.{0} is None else self.{0}.to_dict()' 'None if self.{0} is None else self.{0}.to_dict()',
.format(arg.name) arg.name
) )
builder.writeln() builder.writeln()
@ -421,17 +411,16 @@ class TLGenerator:
.format(a.name) for a in ra) .format(a.name) for a in ra)
builder.writeln( builder.writeln(
"assert ({}) or ({}), '{} parameters must all " "assert ({}) or ({}), '{} parameters must all "
"be False-y (like None) or all me True-y'".format( "be False-y (like None) or all me True-y'",
' and '.join(cnd1), ' and '.join(cnd2), ' and '.join(cnd1), ' and '.join(cnd2),
', '.join(a.name for a in ra) ', '.join(a.name for a in ra)
)
) )
builder.writeln("return b''.join((") builder.writeln("return b''.join((")
builder.current_indent += 1 builder.current_indent += 1
# First constructor code, we already know its bytes # First constructor code, we already know its bytes
builder.writeln('{},'.format(repr(struct.pack('<I', tlobject.id)))) builder.writeln('{},', repr(struct.pack('<I', tlobject.id)))
for arg in tlobject.args: for arg in tlobject.args:
if TLGenerator.write_to_bytes(builder, arg, tlobject.args): if TLGenerator.write_to_bytes(builder, arg, tlobject.args):
@ -449,12 +438,14 @@ class TLGenerator:
builder, arg, tlobject.args, name='_' + arg.name builder, arg, tlobject.args, name='_' + arg.name
) )
builder.writeln('return {}({})'.format( builder.writeln(
tlobject.class_name(), ', '.join( 'return {}({})',
tlobject.class_name(),
', '.join(
'{0}=_{0}'.format(a.name) for a in tlobject.sorted_args() '{0}=_{0}'.format(a.name) for a in tlobject.sorted_args()
if not a.flag_indicator and not a.generic_definition if not a.flag_indicator and not a.generic_definition
) )
)) )
# Only requests can have a different response that's not their # Only requests can have a different response that's not their
# serialized body, that is, we'll be setting their .result. # serialized body, that is, we'll be setting their .result.
@ -482,13 +473,13 @@ class TLGenerator:
@staticmethod @staticmethod
def _write_self_assign(builder, arg, get_input_code): def _write_self_assign(builder, arg, get_input_code):
"""Writes self.arg = input.format(self.arg), considering vectors""" """Writes self.arg = input.format(self.arg), considering vectors."""
if arg.is_vector: if arg.is_vector:
builder.write('self.{0} = [{1} for _x in self.{0}]' builder.write('self.{0} = [{1} for _x in self.{0}]',
.format(arg.name, get_input_code.format('_x'))) arg.name, get_input_code.format('_x'))
else: else:
builder.write('self.{} = {}'.format( builder.write('self.{} = {}',
arg.name, get_input_code.format('self.' + arg.name))) arg.name, get_input_code.format('self.' + arg.name))
builder.writeln( builder.writeln(
' if self.{} else None'.format(arg.name) if arg.is_flag else '' ' if self.{} else None'.format(arg.name) if arg.is_flag else ''
@ -536,17 +527,17 @@ class TLGenerator:
# so we need an extra join here. Note that empty vector flags # so we need an extra join here. Note that empty vector flags
# should NOT be sent either! # should NOT be sent either!
builder.write("b'' if {0} is None or {0} is False " builder.write("b'' if {0} is None or {0} is False "
"else b''.join((".format(name)) "else b''.join((", name)
else: else:
builder.write("b'' if {0} is None or {0} is False " builder.write("b'' if {0} is None or {0} is False "
"else (".format(name)) "else (", name)
if arg.is_vector: if arg.is_vector:
if arg.use_vector_id: if arg.use_vector_id:
# vector code, unsigned 0x1cb5c415 as little endian # vector code, unsigned 0x1cb5c415 as little endian
builder.write(r"b'\x15\xc4\xb5\x1c',") builder.write(r"b'\x15\xc4\xb5\x1c',")
builder.write("struct.pack('<i', len({})),".format(name)) builder.write("struct.pack('<i', len({})),", name)
# Cannot unpack the values for the outer tuple through *[( # Cannot unpack the values for the outer tuple through *[(
# since that's a Python >3.5 feature, so add another join. # since that's a Python >3.5 feature, so add another join.
@ -560,7 +551,7 @@ class TLGenerator:
arg.is_vector = True arg.is_vector = True
arg.is_flag = old_flag arg.is_flag = old_flag
builder.write(' for x in {})'.format(name)) builder.write(' for x in {})', name)
elif arg.flag_indicator: elif arg.flag_indicator:
# Calculate the flags with those items which are not None # Calculate the flags with those items which are not None
@ -579,41 +570,39 @@ class TLGenerator:
elif 'int' == arg.type: elif 'int' == arg.type:
# struct.pack is around 4 times faster than int.to_bytes # struct.pack is around 4 times faster than int.to_bytes
builder.write("struct.pack('<i', {})".format(name)) builder.write("struct.pack('<i', {})", name)
elif 'long' == arg.type: elif 'long' == arg.type:
builder.write("struct.pack('<q', {})".format(name)) builder.write("struct.pack('<q', {})", name)
elif 'int128' == arg.type: elif 'int128' == arg.type:
builder.write("{}.to_bytes(16, 'little', signed=True)".format(name)) builder.write("{}.to_bytes(16, 'little', signed=True)", name)
elif 'int256' == arg.type: elif 'int256' == arg.type:
builder.write("{}.to_bytes(32, 'little', signed=True)".format(name)) builder.write("{}.to_bytes(32, 'little', signed=True)", name)
elif 'double' == arg.type: elif 'double' == arg.type:
builder.write("struct.pack('<d', {})".format(name)) builder.write("struct.pack('<d', {})", name)
elif 'string' == arg.type: elif 'string' == arg.type:
builder.write('TLObject.serialize_bytes({})'.format(name)) builder.write('TLObject.serialize_bytes({})', name)
elif 'Bool' == arg.type: elif 'Bool' == arg.type:
# 0x997275b5 if boolean else 0xbc799737 # 0x997275b5 if boolean else 0xbc799737
builder.write( builder.write(r"b'\xb5ur\x99' if {} else b'7\x97y\xbc'", name)
r"b'\xb5ur\x99' if {} else b'7\x97y\xbc'".format(name)
)
elif 'true' == arg.type: elif 'true' == arg.type:
pass # These are actually NOT written! Only used for flags pass # These are actually NOT written! Only used for flags
elif 'bytes' == arg.type: elif 'bytes' == arg.type:
builder.write('TLObject.serialize_bytes({})'.format(name)) builder.write('TLObject.serialize_bytes({})', name)
elif 'date' == arg.type: # Custom format elif 'date' == arg.type: # Custom format
builder.write('TLObject.serialize_datetime({})'.format(name)) builder.write('TLObject.serialize_datetime({})', name)
else: else:
# Else it may be a custom type # Else it may be a custom type
builder.write('bytes({})'.format(name)) builder.write('bytes({})', name)
if arg.is_flag: if arg.is_flag:
builder.write(')') builder.write(')')
@ -646,15 +635,12 @@ class TLGenerator:
# Treat 'true' flags as a special case, since they're true if # Treat 'true' flags as a special case, since they're true if
# they're set, and nothing else needs to actually be read. # they're set, and nothing else needs to actually be read.
if 'true' == arg.type: if 'true' == arg.type:
builder.writeln( builder.writeln('{} = bool(flags & {})',
'{} = bool(flags & {})'.format(name, 1 << arg.flag_index) name, 1 << arg.flag_index)
)
return return
was_flag = True was_flag = True
builder.writeln('if flags & {}:'.format( builder.writeln('if flags & {}:', 1 << arg.flag_index)
1 << arg.flag_index
))
# Temporary disable .is_flag not to enter this if # Temporary disable .is_flag not to enter this if
# again when calling the method recursively # again when calling the method recursively
arg.is_flag = False arg.is_flag = False
@ -664,12 +650,12 @@ class TLGenerator:
# We have to read the vector's constructor ID # We have to read the vector's constructor ID
builder.writeln("reader.read_int()") builder.writeln("reader.read_int()")
builder.writeln('{} = []'.format(name)) builder.writeln('{} = []', name)
builder.writeln('for _ in range(reader.read_int()):') builder.writeln('for _ in range(reader.read_int()):')
# Temporary disable .is_vector, not to enter this if again # Temporary disable .is_vector, not to enter this if again
arg.is_vector = False arg.is_vector = False
TLGenerator.write_read_code(builder, arg, args, name='_x') TLGenerator.write_read_code(builder, arg, args, name='_x')
builder.writeln('{}.append(_x)'.format(name)) builder.writeln('{}.append(_x)', name)
arg.is_vector = True arg.is_vector = True
elif arg.flag_indicator: elif arg.flag_indicator:
@ -678,44 +664,40 @@ class TLGenerator:
builder.writeln() builder.writeln()
elif 'int' == arg.type: elif 'int' == arg.type:
builder.writeln('{} = reader.read_int()'.format(name)) builder.writeln('{} = reader.read_int()', name)
elif 'long' == arg.type: elif 'long' == arg.type:
builder.writeln('{} = reader.read_long()'.format(name)) builder.writeln('{} = reader.read_long()', name)
elif 'int128' == arg.type: elif 'int128' == arg.type:
builder.writeln( builder.writeln('{} = reader.read_large_int(bits=128)', name)
'{} = reader.read_large_int(bits=128)'.format(name)
)
elif 'int256' == arg.type: elif 'int256' == arg.type:
builder.writeln( builder.writeln('{} = reader.read_large_int(bits=256)', name)
'{} = reader.read_large_int(bits=256)'.format(name)
)
elif 'double' == arg.type: elif 'double' == arg.type:
builder.writeln('{} = reader.read_double()'.format(name)) builder.writeln('{} = reader.read_double()', name)
elif 'string' == arg.type: elif 'string' == arg.type:
builder.writeln('{} = reader.tgread_string()'.format(name)) builder.writeln('{} = reader.tgread_string()', name)
elif 'Bool' == arg.type: elif 'Bool' == arg.type:
builder.writeln('{} = reader.tgread_bool()'.format(name)) builder.writeln('{} = reader.tgread_bool()', name)
elif 'true' == arg.type: elif 'true' == arg.type:
# Arbitrary not-None value, don't actually read "true" flags # Arbitrary not-None value, don't actually read "true" flags
builder.writeln('{} = True'.format(name)) builder.writeln('{} = True', name)
elif 'bytes' == arg.type: elif 'bytes' == arg.type:
builder.writeln('{} = reader.tgread_bytes()'.format(name)) builder.writeln('{} = reader.tgread_bytes()', name)
elif 'date' == arg.type: # Custom format elif 'date' == arg.type: # Custom format
builder.writeln('{} = reader.tgread_date()'.format(name)) builder.writeln('{} = reader.tgread_date()', name)
else: else:
# Else it may be a custom type # Else it may be a custom type
if not arg.skip_constructor_id: if not arg.skip_constructor_id:
builder.writeln('{} = reader.tgread_object()'.format(name)) builder.writeln('{} = reader.tgread_object()', name)
else: else:
# Import the correct type inline to avoid cyclic imports. # Import the correct type inline to avoid cyclic imports.
# There may be better solutions so that we can just access # There may be better solutions so that we can just access
@ -732,10 +714,9 @@ class TLGenerator:
# file with the same namespace, but since it does no harm # file with the same namespace, but since it does no harm
# and we don't have information about such thing in the # and we don't have information about such thing in the
# method we just ignore that case. # method we just ignore that case.
builder.writeln('from {} import {}'.format(ns, class_name)) builder.writeln('from {} import {}', ns, class_name)
builder.writeln('{} = {}.from_reader(reader)'.format( builder.writeln('{} = {}.from_reader(reader)',
name, class_name name, class_name)
))
# End vector and flag blocks if required (if we opened them before) # End vector and flag blocks if required (if we opened them before)
if arg.is_vector: if arg.is_vector:
@ -744,7 +725,7 @@ class TLGenerator:
if was_flag: if was_flag:
builder.current_indent -= 1 builder.current_indent -= 1
builder.writeln('else:') builder.writeln('else:')
builder.writeln('{} = None'.format(name)) builder.writeln('{} = None', name)
builder.current_indent -= 1 builder.current_indent -= 1
# Restore .is_flag # Restore .is_flag
arg.is_flag = True arg.is_flag = True