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'
]
# Change the default role so we can avoid prefixing everything with :obj:
default_role = "py:obj"
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@ -157,7 +160,7 @@ latex_elements = {
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'Telethon.tex', 'Telethon Documentation',
'Jeff', 'manual'),
author, 'manual'),
]

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).
If you don't have 2FA enabled, but you would like to do so through the library,
take as example the following code snippet:
use ``client.edit_2fa()``.
Be sure to know what you're doing when using this function and
you won't run into any problems.
Take note that if you want to set only the email/hint and leave
the current password unchanged, you need to "redo" the 2fa.
See the examples below:
.. code-block:: python
import os
from hashlib import sha256
from telethon.tl.functions import account
from telethon.tl.types.account import PasswordInputSettings
new_salt = client(account.GetPasswordRequest()).new_salt
salt = new_salt + os.urandom(8) # new random salt
pw = 'secret'.encode('utf-8') # type your new password here
hint = 'hint'
pw_salted = salt + pw + salt
pw_hash = sha256(pw_salted).digest()
result = await client(account.UpdatePasswordSettingsRequest(
current_password_hash=salt,
new_settings=PasswordInputSettings(
new_salt=salt,
new_password_hash=pw_hash,
hint=hint
)
))
Thanks to `Issue 259 <https://github.com/LonamiWebs/Telethon/issues/259>`_
for the tip!
from telethon.errors import EmailUnconfirmedError
# Sets 2FA password for first time:
await client.edit_2fa(new_password='supersecurepassword')
# Changes password:
await client.edit_2fa(current_password='supersecurepassword',
new_password='changedmymind')
# Clears current password (i.e. removes 2FA):
await client.edit_2fa(current_password='changedmymind', new_password=None)
# Sets new password with recovery email:
try:
await client.edit_2fa(new_password='memes and dreams',
email='JohnSmith@example.com')
# Raises error (you need to check your email to complete 2FA setup.)
except EmailUnconfirmedError:
# You can put email checking code here if desired.
pass
# Also take note that unless you remove 2FA or explicitly
# give email parameter again it will keep the last used setting
# Set hint after already setting password:
await client.edit_2fa(current_password='memes and dreams',
new_password='memes and dreams',
hint='It keeps you alive')
__ https://github.com/Anorov/PySocks#installation
__ https://github.com/Anorov/PySocks#usage-1

View File

@ -32,7 +32,7 @@ you're able to just do this:
# Dialogs are the "conversations you have open".
# This method returns a list of Dialog, which
# has the .entity attribute and other information.
dialogs = await client.get_dialogs(limit=200)
dialogs = await client.get_dialogs()
# All of these work and do the same.
lonami = await client.get_entity('lonami')
@ -44,27 +44,17 @@ you're able to just do this:
contact = await client.get_entity('+34xxxxxxxxx')
friend = await client.get_entity(friend_id)
# Using Peer/InputPeer (note that the API may return these)
# users, chats and channels may all have the same ID, so it's
# necessary to wrap (at least) chat and channels inside Peer.
#
# NOTICE how the IDs *must* be wrapped inside a Peer() so the
# library knows their type.
# Getting entities through their ID (User, Chat or Channel)
entity = await client.get_entity(some_id)
# You can be more explicit about the type for said ID by wrapping
# it inside a Peer instance. This is recommended but not necessary.
from telethon.tl.types import PeerUser, PeerChat, PeerChannel
my_user = await client.get_entity(PeerUser(some_id))
my_chat = await client.get_entity(PeerChat(some_id))
my_channel = await client.get_entity(PeerChannel(some_id))
.. warning::
As it has been mentioned already, getting the entity of a channel
through e.g. ``client.get_entity(channel id)`` will **not** work.
You would use ``client.get_entity(types.PeerChannel(channel id))``.
Remember that supergroups are channels and normal groups are chats.
This is a common mistake!
All methods in the :ref:`telegram-client` call ``.get_input_entity()`` prior
to sending the requst to save you from the hassle of doing so manually.
That way, convenience calls such as ``client.send_message('lonami', 'hi!')``

View File

@ -14,6 +14,36 @@ it can take advantage of new goodies!
.. contents:: List of All Versions
Several bug fixes (v0.18.2)
===========================
*Published at 2018/03/27*
Just a few bug fixes before they become too many.
Additions
~~~~~~~~~
- Getting an entity by its positive ID should be enough, regardless of their
type (whether it's an ``User``, a ``Chat`` or a ``Channel``). Although
wrapping them inside a ``Peer`` is still recommended, it's not necessary.
- New ``client.edit_2fa`` function to change your Two Factor Authentication
settings.
- ``.stringify()`` and string representation for custom ``Dialog/Draft``.
Bug fixes
~~~~~~~~~
- Some bug regarding ``.get_input_peer``.
- ``events.ChatAction`` wasn't picking up all the pins.
- ``force_document=True`` was being ignored for albums.
- Now you're able to send ``Photo`` and ``Document`` as files.
- Wrong access to a member on chat forbidden error for ``.get_participants``.
An empty list is returned instead.
- ``me/self`` check for ``.get[_input]_entity`` has been moved up so if
someone has "me" or "self" as their name they won't be retrieved.
Iterator methods (v0.18.1)
==========================

View File

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

View File

@ -11,18 +11,27 @@ Working with messages
Forwarding messages
*******************
Note that ForwardMessageRequest_ (note it's Message, singular) will *not*
work if channels are involved. This is because channel (and megagroups) IDs
are not unique, so you also need to know who the sender is (a parameter this
request doesn't have).
Either way, you are encouraged to use ForwardMessagesRequest_ (note it's
Message*s*, plural) *always*, since it is more powerful, as follows:
This request is available as a friendly method through
`client.forward_messages <telethon.telegram_client.TelegramClient.forward_messages>`,
and can be used like shown below:
.. code-block:: python
# If you only have the message IDs
await client.forward_messages(
entity, # to which entity you are forwarding the messages
message_ids, # the IDs of the messages (or message) to forward
from_entity # who sent the messages?
)
# If you have ``Message`` objects
await client.forward_messages(
entity, # to which entity you are forwarding the messages
messages # the messages (or message) to forward
)
# You can also do it manually if you prefer
from telethon.tl.functions.messages import ForwardMessagesRequest
# note the s ^
messages = foo() # retrieve a few messages (or even one, in a list)
from_entity = bar()
@ -119,7 +128,6 @@ send yourself the very first sticker you have:
))
.. _ForwardMessageRequest: https://lonamiwebs.github.io/Telethon/methods/messages/forward_message.html
.. _ForwardMessagesRequest: https://lonamiwebs.github.io/Telethon/methods/messages/forward_messages.html
.. _SearchRequest: https://lonamiwebs.github.io/Telethon/methods/messages/search.html
.. _issues: https://github.com/LonamiWebs/Telethon/issues/215

View File

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

View File

@ -56,6 +56,19 @@ class NotFoundError(RPCError):
self.message = message
class AuthKeyError(RPCError):
"""
Errors related to invalid authorization key, like
AUTH_KEY_DUPLICATED which can cause the connection to fail.
"""
code = 406
message = 'AUTH_KEY'
def __init__(self, message):
super().__init__(message)
self.message = message
class FloodError(RPCError):
"""
The maximum allowed number of attempts to invoke the given method

View File

@ -46,11 +46,11 @@ class _EventBuilder(abc.ABC):
The common event builder, with builtin support to filter per chat.
Args:
chats (:obj:`entity`, optional):
chats (`entity`, optional):
May be one or more entities (username/peer/etc.). By default,
only matching chats will be handled.
blacklist_chats (:obj:`bool`, optional):
blacklist_chats (`bool`, optional):
Whether to treat the chats as a blacklist instead of
as a whitelist (default). This means that every chat
will be handled *except* those specified in ``chats``
@ -118,11 +118,15 @@ class _EventCommon(abc.ABC):
try:
if isinstance(chat, types.InputPeerChannel):
result = await self._client(
functions.channels.GetMessagesRequest(chat, [msg_id])
functions.channels.GetMessagesRequest(chat, [
types.InputMessageID(msg_id)
])
)
else:
result = await self._client(
functions.messages.GetMessagesRequest([msg_id])
functions.messages.GetMessagesRequest([
types.InputMessageID(msg_id)
])
)
except RPCError:
return None, None
@ -228,15 +232,15 @@ class NewMessage(_EventBuilder):
Represents a new message event builder.
Args:
incoming (:obj:`bool`, optional):
incoming (`bool`, optional):
If set to ``True``, only **incoming** messages will be handled.
Mutually exclusive with ``outgoing`` (can only set one of either).
outgoing (:obj:`bool`, optional):
outgoing (`bool`, optional):
If set to ``True``, only **outgoing** messages will be handled.
Mutually exclusive with ``incoming`` (can only set one of either).
pattern (:obj:`str`, :obj:`callable`, :obj:`Pattern`, optional):
pattern (`str`, `callable`, `Pattern`, optional):
If set, only messages matching this pattern will be handled.
You can specify a regex-like string which will be matched
against the message, a callable function that returns ``True``
@ -300,7 +304,7 @@ class NewMessage(_EventBuilder):
else:
return
event._entities = update.entities
event._entities = update._entities
return self._message_filter_event(event)
def _message_filter_event(self, event):
@ -330,16 +334,16 @@ class NewMessage(_EventBuilder):
message (:tl:`Message`):
This is the original :tl:`Message` object.
is_private (:obj:`bool`):
is_private (`bool`):
True if the message was sent as a private message.
is_group (:obj:`bool`):
is_group (`bool`):
True if the message was sent on a group or megagroup.
is_channel (:obj:`bool`):
is_channel (`bool`):
True if the message was sent on a megagroup or channel.
is_reply (:obj:`str`):
is_reply (`str`):
Whether the message is a reply to some other or not.
"""
def __init__(self, message):
@ -501,11 +505,13 @@ class NewMessage(_EventBuilder):
if self._reply_message is None:
if isinstance(await self.input_chat, types.InputPeerChannel):
r = await self._client(functions.channels.GetMessagesRequest(
await self.input_chat, [self.message.reply_to_msg_id]
await self.input_chat, [
types.InputMessageID(self.message.reply_to_msg_id)
]
))
else:
r = await self._client(functions.messages.GetMessagesRequest(
[self.message.reply_to_msg_id]
[types.InputMessageID(self.message.reply_to_msg_id)]
))
if not isinstance(r, types.messages.MessagesNotModified):
self._reply_message = r.messages[0]
@ -692,7 +698,7 @@ class ChatAction(_EventBuilder):
else:
return
event._entities = update.entities
event._entities = update._entities
return self._filter_event(event)
class Event(_EventCommon):
@ -700,35 +706,35 @@ class ChatAction(_EventBuilder):
Represents the event of a new chat action.
Members:
new_pin (:obj:`bool`):
new_pin (`bool`):
``True`` if there is a new pin.
new_photo (:obj:`bool`):
new_photo (`bool`):
``True`` if there's a new chat photo (or it was removed).
photo (:tl:`Photo`, optional):
The new photo (or ``None`` if it was removed).
user_added (:obj:`bool`):
user_added (`bool`):
``True`` if the user was added by some other.
user_joined (:obj:`bool`):
user_joined (`bool`):
``True`` if the user joined on their own.
user_left (:obj:`bool`):
user_left (`bool`):
``True`` if the user left on their own.
user_kicked (:obj:`bool`):
user_kicked (`bool`):
``True`` if the user was kicked by some other.
created (:obj:`bool`, optional):
created (`bool`, optional):
``True`` if this chat was just created.
new_title (:obj:`bool`, optional):
new_title (`bool`, optional):
The new title string for the chat, if applicable.
unpin (:obj:`bool`):
unpin (`bool`):
``True`` if the existing pin gets unpinned.
"""
def __init__(self, where, new_pin=None, new_photo=None,
@ -820,7 +826,9 @@ class ChatAction(_EventBuilder):
if isinstance(self._pinned_message, int) and await self.input_chat:
r = await self._client(functions.channels.GetMessagesRequest(
self._input_chat, [self._pinned_message]
self._input_chat, [
types.InputMessageID(self._pinned_message)
]
))
try:
self._pinned_message = next(
@ -941,7 +949,7 @@ class UserUpdate(_EventBuilder):
else:
return
event._entities = update.entities
event._entities = update._entities
return self._filter_event(event)
class Event(_EventCommon):
@ -949,62 +957,62 @@ class UserUpdate(_EventBuilder):
Represents the event of an user status update (last seen, joined).
Members:
online (:obj:`bool`, optional):
online (`bool`, optional):
``True`` if the user is currently online, ``False`` otherwise.
Might be ``None`` if this information is not present.
last_seen (:obj:`datetime`, optional):
last_seen (`datetime`, optional):
Exact date when the user was last seen if known.
until (:obj:`datetime`, optional):
until (`datetime`, optional):
Until when will the user remain online.
within_months (:obj:`bool`):
within_months (`bool`):
``True`` if the user was seen within 30 days.
within_weeks (:obj:`bool`):
within_weeks (`bool`):
``True`` if the user was seen within 7 days.
recently (:obj:`bool`):
recently (`bool`):
``True`` if the user was seen within a day.
action (:tl:`SendMessageAction`, optional):
The "typing" action if any the user is performing if any.
cancel (:obj:`bool`):
cancel (`bool`):
``True`` if the action was cancelling other actions.
typing (:obj:`bool`):
typing (`bool`):
``True`` if the action is typing a message.
recording (:obj:`bool`):
recording (`bool`):
``True`` if the action is recording something.
uploading (:obj:`bool`):
uploading (`bool`):
``True`` if the action is uploading something.
playing (:obj:`bool`):
playing (`bool`):
``True`` if the action is playing a game.
audio (:obj:`bool`):
audio (`bool`):
``True`` if what's being recorded/uploaded is an audio.
round (:obj:`bool`):
round (`bool`):
``True`` if what's being recorded/uploaded is a round video.
video (:obj:`bool`):
video (`bool`):
``True`` if what's being recorded/uploaded is an video.
document (:obj:`bool`):
document (`bool`):
``True`` if what's being uploaded is document.
geo (:obj:`bool`):
geo (`bool`):
``True`` if what's being uploaded is a geo.
photo (:obj:`bool`):
photo (`bool`):
``True`` if what's being uploaded is a photo.
contact (:obj:`bool`):
contact (`bool`):
``True`` if what's being uploaded (selected) is a contact.
"""
def __init__(self, user_id, status=None, typing=None):
@ -1090,7 +1098,7 @@ class MessageEdited(NewMessage):
else:
return
event._entities = update.entities
event._entities = update._entities
return self._message_filter_event(event)
class Event(NewMessage.Event):
@ -1116,7 +1124,7 @@ class MessageDeleted(_EventBuilder):
else:
return
event._entities = update.entities
event._entities = update._entities
return self._filter_event(event)
class Event(_EventCommon):
@ -1128,6 +1136,140 @@ class MessageDeleted(_EventBuilder):
self.deleted_ids = deleted_ids
@_name_inner_event
class MessageRead(_EventBuilder):
"""
Event fired when one or more messages have been read.
Args:
inbox (`bool`, optional):
If this argument is ``True``, then when you read someone else's
messages the event will be fired. By default (``False``) only
when messages you sent are read by someone else will fire it.
"""
def __init__(self, inbox=False, chats=None, blacklist_chats=None):
super().__init__(chats, blacklist_chats)
self.inbox = inbox
def build(self, update):
if isinstance(update, types.UpdateReadHistoryInbox):
event = MessageRead.Event(update.peer, update.max_id, False)
elif isinstance(update, types.UpdateReadHistoryOutbox):
event = MessageRead.Event(update.peer, update.max_id, True)
elif isinstance(update, types.UpdateReadChannelInbox):
event = MessageRead.Event(types.PeerChannel(update.channel_id),
update.max_id, False)
elif isinstance(update, types.UpdateReadChannelOutbox):
event = MessageRead.Event(types.PeerChannel(update.channel_id),
update.max_id, True)
elif isinstance(update, types.UpdateReadMessagesContents):
event = MessageRead.Event(message_ids=update.messages,
contents=True)
elif isinstance(update, types.UpdateChannelReadMessagesContents):
event = MessageRead.Event(types.PeerChannel(update.channel_id),
message_ids=update.messages,
contents=True)
else:
return
if self.inbox == event.outbox:
return
event._entities = update._entities
return self._filter_event(event)
class Event(_EventCommon):
"""
Represents the event of one or more messages being read.
Members:
max_id (`int`):
Up to which message ID has been read. Every message
with an ID equal or lower to it have been read.
outbox (`bool`):
``True`` if someone else has read your messages.
contents (`bool`):
``True`` if what was read were the contents of a message.
This will be the case when e.g. you play a voice note.
It may only be set on ``inbox`` events.
"""
def __init__(self, peer=None, max_id=None, out=False, contents=False,
message_ids=None):
self.outbox = out
self.contents = contents
self._message_ids = message_ids or []
self._messages = None
self.max_id = max_id or max(message_ids or [], default=None)
super().__init__(peer, self.max_id)
@property
def inbox(self):
"""
``True`` if you have read someone else's messages.
"""
return not self.outbox
@property
def message_ids(self):
"""
The IDs of the messages **which contents'** were read.
Use :meth:`is_read` if you need to check whether a message
was read instead checking if it's in here.
"""
return self._message_ids
@property
async def messages(self):
"""
The list of :tl:`Message` **which contents'** were read.
Use :meth:`is_read` if you need to check whether a message
was read instead checking if it's in here.
"""
if self._messages is None:
chat = self.input_chat
if not chat:
self._messages = []
elif isinstance(chat, types.InputPeerChannel):
ids = [types.InputMessageID(x) for x in self._message_ids]
self._messages =\
await self._client(functions.channels.GetMessagesRequest(
chat, ids
)).messages
else:
ids = [types.InputMessageID(x) for x in self._message_ids]
self._messages =\
await self._client(functions.messages.GetMessagesRequest(
ids
)).messages
return self._messages
def is_read(self, message):
"""
Returns ``True`` if the given message (or its ID) has been read.
If a list-like argument is provided, this method will return a
list of booleans indicating which messages have been read.
"""
if utils.is_list_like(message):
return [(m if isinstance(m, int) else m.id) <= self.max_id
for m in message]
else:
return (message if isinstance(message, int)
else message.id) <= self.max_id
def __contains__(self, message):
"""``True`` if the message(s) are read message."""
if utils.is_list_like(message):
return all(self.is_read(message))
else:
return self.is_read(message)
class StopPropagation(Exception):
"""
If this exception is raised in any of the handlers for a given event,

View File

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

View File

@ -9,7 +9,7 @@ from .crypto import rsa
from .errors import (
RPCError, BrokenAuthKeyError, ServerError, FloodWaitError,
FloodTestPhoneWaitError, TypeNotFoundError, UnauthorizedError,
PhoneMigrateError, NetworkMigrateError, UserMigrateError
PhoneMigrateError, NetworkMigrateError, UserMigrateError, AuthKeyError
)
from .network import authenticator, MtProtoSender, Connection, ConnectionMode
from .sessions import Session, SQLiteSession
@ -217,6 +217,15 @@ class TelegramBareClient:
self.disconnect()
return await self.connect(_sync_updates=_sync_updates)
except AuthKeyError as e:
# As of late March 2018 there were two AUTH_KEY_DUPLICATED
# reports. Retrying with a clean auth_key should fix this.
__log__.warning('Auth key error %s. Clearing it and retrying.', e)
self.disconnect()
self.session.auth_key = None
self.session.save()
return self.connect(_sync_updates=_sync_updates)
except (RPCError, ConnectionError) as e:
# Probably errors from the previous session, ignore them
__log__.error('Connection failed due to %s', e)

View File

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

View File

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

View File

@ -1,7 +1,9 @@
import datetime
from .. import TLObject
from ..functions.messages import SaveDraftRequest
from ..types import UpdateDraftMessage, DraftMessage
from ...errors import RPCError
from ...extensions import markdown
@ -12,13 +14,13 @@ class Draft:
instances of this class when calling :meth:`get_drafts()`.
Args:
date (:obj:`datetime`):
date (`datetime`):
The date of the draft.
link_preview (:obj:`bool`):
link_preview (`bool`):
Whether the link preview is enabled or not.
reply_to_msg_id (:obj:`int`):
reply_to_msg_id (`int`):
The message ID that the draft will reply to.
"""
def __init__(self, client, peer, draft):
@ -142,3 +144,24 @@ class Draft:
Deletes this draft, and returns ``True`` on success.
"""
return await self.set_message(text='')
def to_dict(self):
try:
entity = self.entity
except RPCError as e:
entity = e
return {
'_': 'Draft',
'text': self.text,
'entity': entity,
'date': self.date,
'link_preview': self.link_preview,
'reply_to_msg_id': self.reply_to_msg_id
}
def __str__(self):
return TLObject.pretty_format(self.to_dict())
def stringify(self):
return TLObject.pretty_format(self.to_dict(), indent=0)

View File

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

View File

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

View File

@ -1,3 +1,3 @@
# Versions should comply with PEP440.
# 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',
403: 'ForbiddenError',
404: 'NotFoundError',
406: 'AuthKeyError',
420: 'FloodError',
500: 'ServerError',
}

View File

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

View File

@ -556,7 +556,7 @@ accountDaysTTL#b8d0afdf days:int = AccountDaysTTL;
documentAttributeImageSize#6c37c15c w:int h:int = DocumentAttribute;
documentAttributeAnimated#11b58939 = DocumentAttribute;
documentAttributeSticker#6319d612 flags:# mask:flags.1?true alt:string stickerset:InputStickerSet mask_coords:flags.0?MaskCoords = DocumentAttribute;
documentAttributeVideo#ef02ce6 flags:# round_message:flags.0?true duration:int w:int h:int = DocumentAttribute;
documentAttributeVideo#ef02ce6 flags:# round_message:flags.0?true supports_streaming:flags.1?true duration:int w:int h:int = DocumentAttribute;
documentAttributeAudio#9852f9c6 flags:# voice:flags.10?true duration:int title:flags.0?string performer:flags.1?string waveform:flags.2?bytes = DocumentAttribute;
documentAttributeFilename#15590068 file_name:string = DocumentAttribute;
documentAttributeHasStickers#9801d2f7 = DocumentAttribute;
@ -938,7 +938,15 @@ recentMeUrlStickerSet#bc0a57dc url:string set:StickerSetCovered = RecentMeUrl;
help.recentMeUrls#e0310d7 urls:Vector<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---
@ -993,6 +1001,9 @@ account.updatePasswordSettings#fa7c4b86 current_password_hash:bytes new_settings
account.sendConfirmPhoneCode#1516d7bd flags:# allow_flashcall:flags.0?true hash:string current_number:flags.0?Bool = auth.SentCode;
account.confirmPhone#5f2178c3 phone_code_hash:string phone_code:string = Bool;
account.getTmpPassword#4a82327e password_hash:bytes period:int = account.TmpPassword;
account.getWebAuthorizations#182e6d6f = account.WebAuthorizations;
account.resetWebAuthorization#2d01b9ef hash:long = Bool;
account.resetWebAuthorizations#682d2594 = Bool;
users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;
users.getFullUser#ca30a5b1 id:InputUser = UserFull;
@ -1013,7 +1024,7 @@ contacts.getTopPeers#d4982db5 flags:# correspondents:flags.0?true bots_pm:flags.
contacts.resetTopPeerRating#1ae373ac category:TopPeerCategory peer:InputPeer = Bool;
contacts.resetSaved#879537f1 = Bool;
messages.getMessages#4222fa74 id:Vector<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.getHistory#dcbb8260 peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:int = messages.Messages;
messages.search#39e9ea0 flags:# peer:InputPeer q:string from_id:flags.0?InputUser filter:MessagesFilter min_date:int max_date:int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages;
@ -1141,7 +1152,7 @@ channels.readHistory#cc104937 channel:InputChannel max_id:int = Bool;
channels.deleteMessages#84c1fd4e channel:InputChannel id:Vector<int> = messages.AffectedMessages;
channels.deleteUserHistory#d10dd71b channel:InputChannel user_id:InputUser = messages.AffectedHistory;
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.getParticipant#546dd7a6 channel:InputChannel user_id:InputUser = channels.ChannelParticipant;
channels.getChannels#a7f6bbb id:Vector<InputChannel> = messages.Chats;

View File

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