mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-08-03 19:50:15 +03:00
Merge branch 'master' into asyncio
This commit is contained in:
commit
1eb418e1ab
|
@ -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'),
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!')``
|
||||
|
|
|
@ -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)
|
||||
==========================
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
# Versions should comply with PEP440.
|
||||
# This line is parsed in setup.py:
|
||||
__version__ = '0.18.1'
|
||||
__version__ = '0.18.2'
|
||||
|
|
|
@ -11,6 +11,7 @@ known_base_classes = {
|
|||
401: 'UnauthorizedError',
|
||||
403: 'ForbiddenError',
|
||||
404: 'NotFoundError',
|
||||
406: 'AuthKeyError',
|
||||
420: 'FloodError',
|
||||
500: 'ServerError',
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user