Merge branch 'master' into asyncio

This commit is contained in:
Lonami Exo 2018-01-05 17:59:36 +01:00
commit a1d497a2c0
67 changed files with 2920 additions and 1860 deletions

View File

@ -53,16 +53,16 @@ if you're new with ``asyncio``.
await client.send_file('username', '/home/myself/Pictures/holidays.jpg') await client.send_file('username', '/home/myself/Pictures/holidays.jpg')
await client.download_profile_photo(me) await client.download_profile_photo(me)
total, messages, senders = await client.get_message_history('username') messages = await client.get_message_history('username')
await client.download_media(messages[0]) await client.download_media(messages[0])
Next steps Next steps
---------- ----------
Do you like how Telethon looks? Check the Do you like how Telethon looks? Check out
`wiki over GitHub <https://github.com/LonamiWebs/Telethon/wiki>`_ for a `Read The Docs <http://telethon.rtfd.io/>`_
more in-depth explanation, with examples, troubleshooting issues, and more for a more in-depth explanation, with examples, troubleshooting issues,
useful information. Note that the examples there are written for the threaded and more useful information. Note that the examples there are written for
version, not the one using asyncio. However, you just need to await every the threaded version, not the one using asyncio. However, you just need to
remote call. await every remote call.

View File

@ -90,7 +90,7 @@ class DocsWriter:
def end_menu(self): def end_menu(self):
"""Ends an opened menu""" """Ends an opened menu"""
if not self.menu_began: if not self.menu_began:
raise ValueError('No menu had been started in the first place.') raise RuntimeError('No menu had been started in the first place.')
self.write('</ul>') self.write('</ul>')
def write_title(self, title, level=1): def write_title(self, title, level=1):

View File

@ -1,33 +1,41 @@
.. _accessing-the-full-api: .. _accessing-the-full-api:
========================== ======================
Accessing the Full API Accessing the Full API
========================== ======================
The ``TelegramClient`` doesnt offer a method for every single request
the Telegram API supports. However, its very simple to ``.invoke()`` The ``TelegramClient`` doesn't offer a method for every single request
any request. Whenever you need something, dont forget to `check the the Telegram API supports. However, it's very simple to *call* or *invoke*
any request. Whenever you need something, don't forget to `check the
documentation`__ and look for the `method you need`__. There you can go documentation`__ and look for the `method you need`__. There you can go
through a sorted list of everything you can do. through a sorted list of everything you can do.
.. note::
Removing the hand crafted documentation for methods is still
a work in progress!
You should also refer to the documentation to see what the objects You should also refer to the documentation to see what the objects
(constructors) Telegram returns look like. Every constructor inherits (constructors) Telegram returns look like. Every constructor inherits
from a common type, and thats the reason for this distinction. from a common type, and that's the reason for this distinction.
Say ``client.send_message()`` didnt exist, we could use the `search`__ Say ``client.send_message()`` didn't exist, we could use the `search`__
to look for “message”. There we would find `SendMessageRequest`__, to look for "message". There we would find `SendMessageRequest`__,
which we can work with. which we can work with.
Every request is a Python class, and has the parameters needed for you Every request is a Python class, and has the parameters needed for you
to invoke it. You can also call ``help(request)`` for information on to invoke it. You can also call ``help(request)`` for information on
what input parameters it takes. Remember to Copy import to the what input parameters it takes. Remember to "Copy import to the
clipboard”, or your script wont be aware of this class! Now we have: clipboard", or your script won't be aware of this class! Now we have:
.. code-block:: python .. code-block:: python
from telethon.tl.functions.messages import SendMessageRequest from telethon.tl.functions.messages import SendMessageRequest
If youre going to use a lot of these, you may do: If you're going to use a lot of these, you may do:
.. code-block:: python .. code-block:: python
@ -53,20 +61,20 @@ Or we call ``.get_input_entity()``:
peer = client.get_input_entity('someone') peer = client.get_input_entity('someone')
When youre going to invoke an API method, most require you to pass an When you're going to invoke an API method, most require you to pass an
``InputUser``, ``InputChat``, or so on, this is why using ``InputUser``, ``InputChat``, or so on, this is why using
``.get_input_entity()`` is more straightforward (and sometimes ``.get_input_entity()`` is more straightforward (and often
immediate, if you know the ID of the user for instance). If you also immediate, if you've seen the user before, know their ID, etc.).
need to have information about the whole user, use ``.get_entity()`` If you also need to have information about the whole user, use
instead: ``.get_entity()`` instead:
.. code-block:: python .. code-block:: python
entity = client.get_entity('someone') entity = client.get_entity('someone')
In the later case, when you use the entity, the library will cast it to In the later case, when you use the entity, the library will cast it to
its “input” version for you. If you already have the complete user and its "input" version for you. If you already have the complete user and
want to cache its input version so the library doesnt have to do this want to cache its input version so the library doesn't have to do this
every time its used, simply call ``.get_input_peer``: every time its used, simply call ``.get_input_peer``:
.. code-block:: python .. code-block:: python
@ -83,10 +91,9 @@ request we do:
result = client(SendMessageRequest(peer, 'Hello there!')) result = client(SendMessageRequest(peer, 'Hello there!'))
# __call__ is an alias for client.invoke(request). Both will work # __call__ is an alias for client.invoke(request). Both will work
Message sent! Of course, this is only an example. Message sent! Of course, this is only an example. There are nearly 250
There are nearly 250 methods available as of layer 73, methods available as of layer 73, and you can use every single of them
and you can use every single of them as you wish. as you wish. Remember to use the right types! To sum up:
Remember to use the right types! To sum up:
.. code-block:: python .. code-block:: python
@ -97,16 +104,16 @@ Remember to use the right types! To sum up:
.. note:: .. note::
Note that some requests have a "hash" parameter. This is **not** your ``api_hash``! Note that some requests have a "hash" parameter. This is **not**
It likely isn't your self-user ``.access_hash`` either. your ``api_hash``! It likely isn't your self-user ``.access_hash`` either.
It's a special hash used by Telegram to only send a difference of new data
that you don't already have with that request,
so you can leave it to 0, and it should work (which means no hash is known yet).
For those requests having a "limit" parameter, It's a special hash used by Telegram to only send a difference of new data
you can often set it to zero to signify "return as many items as possible". that you don't already have with that request, so you can leave it to 0,
This won't work for all of them though, and it should work (which means no hash is known yet).
for instance, in "messages.search" it will actually return 0 items.
For those requests having a "limit" parameter, you can often set it to
zero to signify "return default amount". This won't work for all of them
though, for instance, in "messages.search" it will actually return 0 items.
__ https://lonamiwebs.github.io/Telethon __ https://lonamiwebs.github.io/Telethon
@ -114,4 +121,4 @@ __ https://lonamiwebs.github.io/Telethon/methods/index.html
__ https://lonamiwebs.github.io/Telethon/?q=message __ https://lonamiwebs.github.io/Telethon/?q=message
__ https://lonamiwebs.github.io/Telethon/methods/messages/send_message.html __ https://lonamiwebs.github.io/Telethon/methods/messages/send_message.html
__ https://lonamiwebs.github.io/Telethon/types/input_peer.html __ https://lonamiwebs.github.io/Telethon/types/input_peer.html
__ https://lonamiwebs.github.io/Telethon/constructors/input_peer_user.html __ https://lonamiwebs.github.io/Telethon/constructors/input_peer_user.html

View File

@ -0,0 +1,46 @@
.. _sessions:
==============
Session Files
==============
The first parameter you pass the the constructor of the ``TelegramClient`` is
the ``session``, and defaults to be the session name (or full path). That is,
if you create a ``TelegramClient('anon')`` instance and connect, an
``anon.session`` file will be created on the working directory.
These database files using ``sqlite3`` contain the required information to
talk to the Telegram servers, such as to which IP the client should connect,
port, authorization key so that messages can be encrypted, and so on.
These files will by default also save all the input entities that you've seen,
so that you can get information about an user or channel by just their ID.
Telegram will **not** send their ``access_hash`` required to retrieve more
information about them, if it thinks you have already seem them. For this
reason, the library needs to store this information offline.
The library will by default too save all the entities (chats and channels
with their name and username, and users with the phone too) in the session
file, so that you can quickly access them by username or phone number.
If you're not going to work with updates, or don't need to cache the
``access_hash`` associated with the entities' ID, you can disable this
by setting ``client.session.save_entities = False``, or pass it as a
parameter to the ``TelegramClient``.
If you don't want to save the files as a database, you can also create
your custom ``Session`` subclass and override the ``.save()`` and ``.load()``
methods. For example, you could save it on a database:
.. code-block:: python
class DatabaseSession(Session):
def save():
# serialize relevant data to the database
def load():
# load relevant data to the database
You should read the ````session.py```` source file to know what "relevant
data" you need to keep track of.

View File

@ -1,58 +0,0 @@
=========================
Signing In
=========================
.. note::
Make sure you have gone through :ref:`prelude` already!
Two Factor Authorization (2FA)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
If you have Two Factor Authorization (from now on, 2FA) enabled on your account, calling
:meth:`telethon.TelegramClient.sign_in` will raise a `SessionPasswordNeededError`.
When this happens, just :meth:`telethon.TelegramClient.sign_in` again with a ``password=``:
.. code-block:: python
import getpass
from telethon.errors import SessionPasswordNeededError
client.sign_in(phone)
try:
client.sign_in(code=input('Enter code: '))
except SessionPasswordNeededError:
client.sign_in(password=getpass.getpass())
Enabling 2FA
*************
If you don't have 2FA enabled, but you would like to do so through Telethon, take as example the following code snippet:
.. 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 = 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!

View File

@ -1,324 +0,0 @@
=========================
Users and Chats
=========================
.. note::
Make sure you have gone through :ref:`prelude` already!
.. contents::
:depth: 2
.. _retrieving-an-entity:
Retrieving an entity (user or group)
**************************************
An “entity” is used to refer to either an `User`__ or a `Chat`__
(which includes a `Channel`__). The most straightforward way to get
an entity is to use ``TelegramClient.get_entity()``. This method accepts
either a string, which can be a username, phone number or `t.me`__-like
link, or an integer that will be the ID of an **user**. You can use it
like so:
.. code-block:: python
# all of these work
lonami = client.get_entity('lonami')
lonami = client.get_entity('t.me/lonami')
lonami = client.get_entity('https://telegram.dog/lonami')
# other kind of entities
channel = client.get_entity('telegram.me/joinchat/AAAAAEkk2WdoDrB4-Q8-gg')
contact = client.get_entity('+34xxxxxxxxx')
friend = client.get_entity(friend_id)
For the last one to work, the library must have “seen” the user at least
once. The library will “see” the user as long as any request contains
them, so if youve called ``.get_dialogs()`` for instance, and your
friend was there, the library will know about them. For more, read about
the :ref:`sessions`.
If you want to get a channel or chat by ID, you need to specify that
they are a channel or a chat. The library cant infer what they are by
just their ID (unless the ID is marked, but this is only done
internally), so you need to wrap the ID around a `Peer`__ object:
.. code-block:: python
from telethon.tl.types import PeerUser, PeerChat, PeerChannel
my_user = client.get_entity(PeerUser(some_id))
my_chat = client.get_entity(PeerChat(some_id))
my_channel = client.get_entity(PeerChannel(some_id))
**Note** that most requests dont ask for an ``User``, or a ``Chat``,
but rather for ``InputUser``, ``InputChat``, and so on. If this is the
case, you should prefer ``.get_input_entity()`` over ``.get_entity()``,
as it will be immediate if you provide an ID (whereas ``.get_entity()``
may need to find who the entity is first).
Via your open “chats” (dialogs)
-------------------------------
.. note::
Please read here: :ref:`retrieving-all-dialogs`.
Via ResolveUsernameRequest
--------------------------
This is the request used by ``.get_entity`` internally, but you can also
use it by hand:
.. code-block:: python
from telethon.tl.functions.contacts import ResolveUsernameRequest
result = client(ResolveUsernameRequest('username'))
found_chats = result.chats
found_users = result.users
# result.peer may be a PeerUser, PeerChat or PeerChannel
See `Peer`__ for more information about this result.
Via MessageFwdHeader
--------------------
If all you have is a `MessageFwdHeader`__ after you retrieved a bunch
of messages, this gives you access to the ``from_id`` (if forwarded from
an user) and ``channel_id`` (if forwarded from a channel). Invoking
`GetMessagesRequest`__ also returns a list of ``chats`` and
``users``, and you can find the desired entity there:
.. code-block:: python
# Logic to retrieve messages with `GetMessagesRequest´
messages = foo()
fwd_header = bar()
user = next(u for u in messages.users if u.id == fwd_header.from_id)
channel = next(c for c in messages.chats if c.id == fwd_header.channel_id)
Or you can just call ``.get_entity()`` with the ID, as you should have
seen that user or channel before. A call to ``GetMessagesRequest`` may
still be neeed.
Via GetContactsRequest
----------------------
The library will call this for you if you pass a phone number to
``.get_entity``, but again, it can be done manually. If the user you
want to talk to is a contact, you can use `GetContactsRequest`__:
.. code-block:: python
from telethon.tl.functions.contacts import GetContactsRequest
from telethon.tl.types.contacts import Contacts
contacts = client(GetContactsRequest(0))
if isinstance(contacts, Contacts):
users = contacts.users
contacts = contacts.contacts
__ https://lonamiwebs.github.io/Telethon/types/user.html
__ https://lonamiwebs.github.io/Telethon/types/chat.html
__ https://lonamiwebs.github.io/Telethon/constructors/channel.html
__ https://t.me
__ https://lonamiwebs.github.io/Telethon/types/peer.html
__ https://lonamiwebs.github.io/Telethon/types/peer.html
__ https://lonamiwebs.github.io/Telethon/constructors/message_fwd_header.html
__ https://lonamiwebs.github.io/Telethon/methods/messages/get_messages.html
__ https://lonamiwebs.github.io/Telethon/methods/contacts/get_contacts.html
.. _retrieving-all-dialogs:
Retrieving all dialogs
***********************
There are several ``offset_xyz=`` parameters that have no effect at all,
but there's not much one can do since this is something the server should handle.
Currently, the only way to get all dialogs
(open chats, conversations, etc.) is by using the ``offset_date``:
.. code-block:: python
from telethon.tl.functions.messages import GetDialogsRequest
from telethon.tl.types import InputPeerEmpty
from time import sleep
dialogs = []
users = []
chats = []
last_date = None
chunk_size = 20
while True:
result = client(GetDialogsRequest(
offset_date=last_date,
offset_id=0,
offset_peer=InputPeerEmpty(),
limit=chunk_size
))
dialogs.extend(result.dialogs)
users.extend(result.users)
chats.extend(result.chats)
if not result.messages:
break
last_date = min(msg.date for msg in result.messages)
sleep(2)
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``.
Joining a public channel
------------------------
Once you have the :ref:`entity <retrieving-an-entity>`
of the channel you want to join to, you can
make use of the `JoinChannelRequest`__ to join such channel:
.. code-block:: python
from telethon.tl.functions.channels import JoinChannelRequest
client(JoinChannelRequest(channel))
# In the same way, you can also leave such channel
from telethon.tl.functions.channels import LeaveChannelRequest
client(LeaveChannelRequest(input_channel))
For more on channels, check the `channels namespace`__.
Joining a private chat or channel
---------------------------------
If all you have is a link like this one:
``https://t.me/joinchat/AAAAAFFszQPyPEZ7wgxLtd``, you already have
enough information to join! The part after the
``https://t.me/joinchat/``, this is, ``AAAAAFFszQPyPEZ7wgxLtd`` on this
example, is the ``hash`` of the chat or channel. Now you can use
`ImportChatInviteRequest`__ as follows:
.. -block:: python
from telethon.tl.functions.messages import ImportChatInviteRequest
updates = client(ImportChatInviteRequest('AAAAAEHbEkejzxUjAUCfYg'))
Adding someone else to such chat or channel
-------------------------------------------
If you dont want to add yourself, maybe because youre already in, you
can always add someone else with the `AddChatUserRequest`__, which
use is very straightforward:
.. code-block:: python
from telethon.tl.functions.messages import AddChatUserRequest
client(AddChatUserRequest(
chat_id,
user_to_add,
fwd_limit=10 # allow the user to see the 10 last messages
))
Checking a link without joining
-------------------------------
If you dont need to join but rather check whether its a group or a
channel, you can use the `CheckChatInviteRequest`__, which takes in
the `hash`__ of said channel or group.
__ https://lonamiwebs.github.io/Telethon/constructors/chat.html
__ https://lonamiwebs.github.io/Telethon/constructors/channel.html
__ https://lonamiwebs.github.io/Telethon/types/chat.html
__ https://lonamiwebs.github.io/Telethon/methods/channels/join_channel.html
__ https://lonamiwebs.github.io/Telethon/methods/channels/index.html
__ https://lonamiwebs.github.io/Telethon/methods/messages/import_chat_invite.html
__ https://lonamiwebs.github.io/Telethon/methods/messages/add_chat_user.html
__ https://lonamiwebs.github.io/Telethon/methods/messages/check_chat_invite.html
__ https://github.com/LonamiWebs/Telethon/wiki/Joining-a-chat-or-channel#joining-a-private-chat-or-channel
Retrieving all chat members (channels too)
******************************************
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 youre going to
use will work), and a mandatory `ChannelParticipantsFilter`__. The
closest thing to “no filter” is to simply use
`ChannelParticipantsSearch`__ with an empty ``'q'`` string.
If we want to get *all* the members, we need to use a moving offset and
a fixed limit:
.. code-block:: python
from telethon.tl.functions.channels import GetParticipantsRequest
from telethon.tl.types import ChannelParticipantsSearch
from time import sleep
offset = 0
limit = 100
all_participants = []
while True:
participants = client.invoke(GetParticipantsRequest(
channel, ChannelParticipantsSearch(''), offset, limit
))
if not participants.users:
break
all_participants.extend(participants.users)
offset += len(participants.users)
# sleep(1) # This line seems to be optional, no guarantees!
Note that ``GetParticipantsRequest`` returns `ChannelParticipants`__,
which may have more information you need (like the role of the
participants, total count of members, etc.)
__ 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
Recent Actions
********************
“Recent actions” is simply the name official applications have given to
the “admin log”. Simply use `GetAdminLogRequest`__ for that, and
youll get AdminLogResults.events in return which in turn has the final
`.action`__.
__ https://lonamiwebs.github.io/Telethon/methods/channels/get_admin_log.html
__ https://lonamiwebs.github.io/Telethon/types/channel_admin_log_event_action.html
Increasing View Count in a Channel
****************************************
It has been asked `quite`__ `a few`__ `times`__ (really, `many`__), and
while I dont understand why so many people ask this, the solution is to
use `GetMessagesViewsRequest`__, setting ``increment=True``:
.. code-block:: python
# Obtain `channel' through dialogs or through client.get_entity() or anyhow.
# Obtain `msg_ids' through `.get_message_history()` or anyhow. Must be a list.
client(GetMessagesViewsRequest(
peer=channel,
id=msg_ids,
increment=True
))
__ https://github.com/LonamiWebs/Telethon/issues/233
__ https://github.com/LonamiWebs/Telethon/issues/305
__ https://github.com/LonamiWebs/Telethon/issues/409
__ https://github.com/LonamiWebs/Telethon/issues/447
__ https://lonamiwebs.github.io/Telethon/methods/messages/get_messages_views.html

View File

@ -1,48 +0,0 @@
.. _prelude:
Prelude
---------
Before reading any specific example, make sure to read the following common steps:
All the examples assume that you have successfully created a client and you're authorized as follows:
.. code-block:: python
from telethon import TelegramClient
# Use your own values here
api_id = 12345
api_hash = '0123456789abcdef0123456789abcdef'
phone_number = '+34600000000'
client = TelegramClient('some_name', api_id, api_hash)
client.connect() # Must return True, otherwise, try again
if not client.is_user_authorized():
client.send_code_request(phone_number)
# .sign_in() may raise PhoneNumberUnoccupiedError
# In that case, you need to call .sign_up() to get a new account
client.sign_in(phone_number, input('Enter code: '))
# The `client´ is now ready
Although Python will probably clean up the resources used by the ``TelegramClient``,
you should always ``.disconnect()`` it once you're done:
.. code-block:: python
try:
# Code using the client goes here
except:
# No matter what happens, always disconnect in the end
client.disconnect()
If the examples aren't enough, you're strongly advised to read the source code
for the InteractiveTelegramClient_ for an overview on how you could build your next script.
This example shows a basic usage more than enough in most cases. Even reading the source
for the TelegramClient_ may help a lot!
.. _InteractiveTelegramClient: https://github.com/LonamiWebs/Telethon/blob/master/telethon_examples/interactive_telegram_client.py
.. _TelegramClient: https://github.com/LonamiWebs/Telethon/blob/master/telethon/telegram_client.py

View File

@ -1,24 +1,28 @@
.. _creating-a-client: .. _creating-a-client:
=================== =================
Creating a Client Creating a Client
=================== =================
Before working with Telegram's API, you need to get your own API ID and hash: Before working with Telegram's API, you need to get your own API ID and hash:
1. Follow `this link <https://my.telegram.org/>`_ and login with your phone number. 1. Follow `this link <https://my.telegram.org/>`_ and login with your
phone number.
2. Click under API Development tools. 2. Click under API Development tools.
3. A *Create new application* window will appear. Fill in your application details. 3. A *Create new application* window will appear. Fill in your application
There is no need to enter any *URL*, and only the first two fields (*App title* and *Short name*) details. There is no need to enter any *URL*, and only the first two
can be changed later as far as I'm aware. fields (*App title* and *Short name*) can currently be changed later.
4. Click on *Create application* at the end. Remember that your **API hash is secret** 4. Click on *Create application* at the end. Remember that your
and Telegram won't let you revoke it. Don't post it anywhere! **API hash is secret** and Telegram won't let you revoke it.
Don't post it anywhere!
Once that's ready, the next step is to create a ``TelegramClient``. Once that's ready, the next step is to create a ``TelegramClient``.
This class will be your main interface with Telegram's API, and creating one is very simple: This class will be your main interface with Telegram's API, and creating
one is very simple:
.. code-block:: python .. code-block:: python
@ -31,14 +35,18 @@ This class will be your main interface with Telegram's API, and creating one is
client = TelegramClient('some_name', api_id, api_hash) client = TelegramClient('some_name', api_id, api_hash)
Note that ``'some_name'`` will be used to save your session (persistent information such as access key and others)
as ``'some_name.session'`` in your disk. This is simply a JSON file which you can (but shouldn't) modify.
Before using the client, you must be connected to Telegram. Doing so is very easy: Note that ``'some_name'`` will be used to save your session (persistent
information such as access key and others) as ``'some_name.session'`` in
your disk. This is by default a database file using Python's ``sqlite3``.
Before using the client, you must be connected to Telegram.
Doing so is very easy:
``client.connect() # Must return True, otherwise, try again`` ``client.connect() # Must return True, otherwise, try again``
You may or may not be authorized yet. You must be authorized before you're able to send any request: You may or may not be authorized yet. You must be authorized
before you're able to send any request:
``client.is_user_authorized() # Returns True if you can send requests`` ``client.is_user_authorized() # Returns True if you can send requests``
@ -52,13 +60,25 @@ If you're not authorized, you need to ``.sign_in()``:
# If .sign_in raises SessionPasswordNeeded error, call .sign_in(password=...) # If .sign_in raises SessionPasswordNeeded error, call .sign_in(password=...)
# You can import both exceptions from telethon.errors. # You can import both exceptions from telethon.errors.
``myself`` is your Telegram user. ``myself`` is your Telegram user. You can view all the information about
You can view all the information about yourself by doing ``print(myself.stringify())``. yourself by doing ``print(myself.stringify())``. You're now ready to use
You're now ready to use the client as you wish! the client as you wish! Remember that any object returned by the API has
mentioned ``.stringify()`` method, and printing these might prove useful.
As a full example:
.. code-block:: python
client = TelegramClient('anon', api_id, api_hash)
assert client.connect()
if not client.is_user_authorized():
client.send_code_request(phone_number)
me = client.sign_in(phone_number, input('Enter code: '))
.. note:: .. note::
If you want to use a **proxy**, you have to `install PySocks`__ (via pip or manual) If you want to use a **proxy**, you have to `install PySocks`__
and then set the appropriated parameters: (via pip or manual) and then set the appropriated parameters:
.. code-block:: python .. code-block:: python
@ -72,5 +92,58 @@ You're now ready to use the client as you wish!
consisting of parameters described `here`__. consisting of parameters described `here`__.
Two Factor Authorization (2FA)
******************************
If you have Two Factor Authorization (from now on, 2FA) enabled on your
account, calling :meth:`telethon.TelegramClient.sign_in` will raise a
``SessionPasswordNeededError``. When this happens, just
:meth:`telethon.TelegramClient.sign_in` again with a ``password=``:
.. code-block:: python
import getpass
from telethon.errors import SessionPasswordNeededError
client.sign_in(phone)
try:
client.sign_in(code=input('Enter code: '))
except SessionPasswordNeededError:
client.sign_in(password=getpass.getpass())
If you don't have 2FA enabled, but you would like to do so through the library,
take as example the following code snippet:
.. 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 = 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!
__ https://github.com/Anorov/PySocks#installation __ https://github.com/Anorov/PySocks#installation
__ https://github.com/Anorov/PySocks#usage-1%3E __ https://github.com/Anorov/PySocks#usage-1

View File

@ -0,0 +1,87 @@
=========================
Users, Chats and Channels
=========================
Introduction
************
The library widely uses the concept of "entities". An entity will refer
to any ``User``, ``Chat`` or ``Channel`` object that the API may return
in response to certain methods, such as ``GetUsersRequest``.
To save bandwidth, the API also makes use of their "input" versions.
The input version of an entity (e.g. ``InputPeerUser``, ``InputChat``,
etc.) only contains the minimum required information that's required
for Telegram to be able to identify who you're referring to: their ID
and hash. This ID/hash pair is unique per user, so if you use the pair
given by another user **or bot** it will **not** work.
To save *even more* bandwidth, the API also makes use of the ``Peer``
versions, which just have an ID. This serves to identify them, but
peers alone are not enough to use them. You need to know their hash
before you can "use them".
Luckily, the library tries to simplify this mess the best it can.
Getting entities
****************
Through the use of the :ref:`sessions`, the library will automatically
remember the ID and hash pair, along with some extra information, so
you're able to just do this:
.. code-block:: python
# Dialogs are the "conversations you have open".
# This method returns a list of Dialog, which
# has the .entity attribute and other information.
dialogs = client.get_dialogs(limit=200)
# All of these work and do the same.
lonami = client.get_entity('lonami')
lonami = client.get_entity('t.me/lonami')
lonami = client.get_entity('https://telegram.dog/lonami')
# Other kind of entities.
channel = client.get_entity('telegram.me/joinchat/AAAAAEkk2WdoDrB4-Q8-gg')
contact = client.get_entity('+34xxxxxxxxx')
friend = 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.
from telethon.tl.types import PeerUser, PeerChat, PeerChannel
my_user = client.get_entity(PeerUser(some_id))
my_chat = client.get_entity(PeerChat(some_id))
my_channel = client.get_entity(PeerChannel(some_id))
All methods in the :ref:`telegram-client` call ``.get_entity()`` to further
save you from the hassle of doing so manually, so doing things like
``client.send_message('lonami', 'hi!')`` is possible.
Every entity the library "sees" (in any response to any call) will by
default be cached in the ``.session`` file, to avoid performing
unnecessary API calls. If the entity cannot be found, some calls
like ``ResolveUsernameRequest`` or ``GetContactsRequest`` may be
made to obtain the required information.
Entities vs. Input Entities
***************************
As we mentioned before, API calls don't need to know the whole information
about the entities, only their ID and hash. For this reason, another method,
``.get_input_entity()`` is available. This will always use the cache while
possible, making zero API calls most of the time. When a request is made,
if you provided the full entity, e.g. an ``User``, the library will convert
it to the required ``InputPeer`` automatically for you.
**You should always favour** ``.get_input_entity()`` **over** ``.get_entity()``
for this reason! Calling the latter will always make an API call to get
the most recent information about said entity, but invoking requests don't
need this information, just the ``InputPeer``. Only use ``.get_entity()``
if you need to get actual information, like the username, name, title, etc.
of the entity.

View File

@ -3,13 +3,13 @@
You can adapt this file completely to your liking, but it should at least You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive. contain the root `toctree` directive.
===============
Getting Started
===============
=================
Getting Started!
=================
Simple Installation Simple Installation
********************* *******************
``pip install telethon`` ``pip install telethon``
@ -17,7 +17,7 @@ Simple Installation
Creating a client Creating a client
************** *****************
.. code-block:: python .. code-block:: python
@ -39,8 +39,9 @@ Creating a client
**More details**: :ref:`creating-a-client` **More details**: :ref:`creating-a-client`
Simple Stuff Basic Usage
************** ***********
.. code-block:: python .. code-block:: python
print(me.stringify()) print(me.stringify())
@ -49,6 +50,7 @@ Simple Stuff
client.send_file('username', '/home/myself/Pictures/holidays.jpg') client.send_file('username', '/home/myself/Pictures/holidays.jpg')
client.download_profile_photo(me) client.download_profile_photo(me)
total, messages, senders = client.get_message_history('username') messages = client.get_message_history('username')
client.download_media(messages[0]) client.download_media(messages[0])
**More details**: :ref:`telegram-client`

View File

@ -1,18 +1,20 @@
.. _installation: .. _installation:
================= ============
Installation Installation
================= ============
Automatic Installation Automatic Installation
^^^^^^^^^^^^^^^^^^^^^^^ **********************
To install Telethon, simply do: To install Telethon, simply do:
``pip install telethon`` ``pip install telethon``
If you get something like ``"SyntaxError: invalid syntax"`` or any other error while installing, If you get something like ``"SyntaxError: invalid syntax"`` or any other
it's probably because ``pip`` defaults to Python 2, which is not supported. Use ``pip3`` instead. error while installing/importing the library, it's probably because ``pip``
defaults to Python 2, which is not supported. Use ``pip3`` instead.
If you already have the library installed, upgrade with: If you already have the library installed, upgrade with:
@ -20,7 +22,7 @@ If you already have the library installed, upgrade with:
You can also install the library directly from GitHub or a fork: You can also install the library directly from GitHub or a fork:
.. code-block:: python .. code-block:: sh
# pip install git+https://github.com/LonamiWebs/Telethon.git # pip install git+https://github.com/LonamiWebs/Telethon.git
or or
@ -32,13 +34,15 @@ If you don't have root access, simply pass the ``--user`` flag to the pip comman
Manual Installation Manual Installation
^^^^^^^^^^^^^^^^^^^^ *******************
1. Install the required ``pyaes`` (`GitHub`__ | `PyPi`__) and ``rsa`` (`GitHub`__ | `PyPi`__) modules: 1. Install the required ``pyaes`` (`GitHub`__ | `PyPi`__) and
``rsa`` (`GitHub`__ | `PyPi`__) modules:
``sudo -H pip install pyaes rsa`` ``sudo -H pip install pyaes rsa``
2. Clone Telethon's GitHub repository: ``git clone https://github.com/LonamiWebs/Telethon.git`` 2. Clone Telethon's GitHub repository:
``git clone https://github.com/LonamiWebs/Telethon.git``
3. Enter the cloned repository: ``cd Telethon`` 3. Enter the cloned repository: ``cd Telethon``
@ -50,22 +54,14 @@ To generate the documentation, ``cd docs`` and then ``python3 generate.py``.
Optional dependencies Optional dependencies
^^^^^^^^^^^^^^^^^^^^^^^^ *********************
If you're using the library under ARM (or even if you aren't),
you may want to install ``sympy`` through ``pip`` for a substantial speed-up
when generating the keys required to connect to Telegram
(you can of course do this on desktop too). See `issue #199`__ for more.
If ``libssl`` is available on your system, it will also be used wherever encryption is needed.
If neither of these are available, a pure Python callback will be used instead,
so you can still run the library wherever Python is available!
If ``libssl`` is available on your system, it will be used wherever encryption
is needed, but otherwise it will fall back to pure Python implementation so it
will also work without it.
__ https://github.com/ricmoo/pyaes __ https://github.com/ricmoo/pyaes
__ https://pypi.python.org/pypi/pyaes __ https://pypi.python.org/pypi/pyaes
__ https://github.com/sybrenstuvel/python-rsa/ __ https://github.com/sybrenstuvel/python-rsa/
__ https://pypi.python.org/pypi/rsa/3.4.2 __ https://pypi.python.org/pypi/rsa/3.4.2
__ https://github.com/LonamiWebs/Telethon/issues/199

View File

@ -1,55 +0,0 @@
.. _sending-requests:
==================
Sending Requests
==================
Since we're working with Python, one must not forget that they can do ``help(client)`` or ``help(TelegramClient)``
at any time for a more detailed description and a list of all the available methods.
Calling ``help()`` from an interactive Python session will always list all the methods for any object, even yours!
Interacting with the Telegram API is done through sending **requests**,
this is, any "method" listed on the API. There are a few methods on the ``TelegramClient`` class
that abstract you from the need of manually importing the requests you need.
For instance, retrieving your own user can be done in a single line:
``myself = client.get_me()``
Internally, this method has sent a request to Telegram, who replied with the information about your own user.
If you want to retrieve any other user, chat or channel (channels are a special subset of chats),
you want to retrieve their "entity". This is how the library refers to either of these:
.. code-block:: python
# The method will infer that you've passed an username
# It also accepts phone numbers, and will get the user
# from your contact list.
lonami = client.get_entity('lonami')
Note that saving and using these entities will be more important when Accessing the Full API.
For now, this is a good way to get information about an user or chat.
Other common methods for quick scripts are also available:
.. code-block:: python
# Sending a message (use an entity/username/etc)
client.send_message('TheAyyBot', 'ayy')
# Sending a photo, or a file
client.send_file(myself, '/path/to/the/file.jpg', force_document=True)
# Downloading someone's profile photo. File is saved to 'where'
where = client.download_profile_photo(someone)
# Retrieving the message history
total, messages, senders = client.get_message_history(someone)
# Downloading the media from a specific message
# You can specify either a directory, a filename, or nothing at all
where = client.download_media(message, '/path/to/output')
Remember that you can call ``.stringify()`` to any object Telegram returns to pretty print it.
Calling ``str(result)`` does the same operation, but on a single line.

View File

@ -1,48 +0,0 @@
.. _sessions:
==============
Session Files
==============
The first parameter you pass the the constructor of the
``TelegramClient`` is the ``session``, and defaults to be the session
name (or full path). That is, if you create a ``TelegramClient('anon')``
instance and connect, an ``anon.session`` file will be created on the
working directory.
These JSON session files contain the required information to talk to the
Telegram servers, such as to which IP the client should connect, port,
authorization key so that messages can be encrypted, and so on.
These files will by default also save all the input entities that youve
seen, so that you can get information about an user or channel by just
their ID. Telegram will **not** send their ``access_hash`` required to
retrieve more information about them, if it thinks you have already seem
them. For this reason, the library needs to store this information
offline.
The library will by default too save all the entities (users with their
name, username, chats and so on) **in memory**, not to disk, so that you
can quickly access them by username or phone number. This can be
disabled too. Run ``help(client.session.entities)`` to see the available
methods (or ``help(EntityDatabase)``).
If youre not going to work without updates, or dont need to cache the
``access_hash`` associated with the entities ID, you can disable this
by setting ``client.session.save_entities = False``.
If you dont want to save the files as JSON, you can also create your
custom ``Session`` subclass and override the ``.save()`` and ``.load()``
methods. For example, you could save it on a database:
.. code-block:: python
class DatabaseSession(Session):
def save():
# serialize relevant data to the database
def load():
# load relevant data to the database
You should read the ``session.py`` source file to know what “relevant
data” you need to keep track of.

View File

@ -0,0 +1,99 @@
.. _telegram-client:
==============
TelegramClient
==============
Introduction
************
The ``TelegramClient`` is the central class of the library, the one
you will be using most of the time. For this reason, it's important
to know what it offers.
Since we're working with Python, one must not forget that we can do
``help(client)`` or ``help(TelegramClient)`` at any time for a more
detailed description and a list of all the available methods. Calling
``help()`` from an interactive Python session will always list all the
methods for any object, even yours!
Interacting with the Telegram API is done through sending **requests**,
this is, any "method" listed on the API. There are a few methods (and
growing!) on the ``TelegramClient`` class that abstract you from the
need of manually importing the requests you need.
For instance, retrieving your own user can be done in a single line:
``myself = client.get_me()``
Internally, this method has sent a request to Telegram, who replied with
the information about your own user, and then the desired information
was extracted from their response.
If you want to retrieve any other user, chat or channel (channels are a
special subset of chats), you want to retrieve their "entity". This is
how the library refers to either of these:
.. code-block:: python
# The method will infer that you've passed an username
# It also accepts phone numbers, and will get the user
# from your contact list.
lonami = client.get_entity('lonami')
The so called "entities" are another important whole concept on its own,
and you should
Note that saving and using these entities will be more important when
Accessing the Full API. For now, this is a good way to get information
about an user or chat.
Other common methods for quick scripts are also available:
.. code-block:: python
# Sending a message (use an entity/username/etc)
client.send_message('TheAyyBot', 'ayy')
# Sending a photo, or a file
client.send_file(myself, '/path/to/the/file.jpg', force_document=True)
# Downloading someone's profile photo. File is saved to 'where'
where = client.download_profile_photo(someone)
# Retrieving the message history
messages = client.get_message_history(someone)
# Downloading the media from a specific message
# You can specify either a directory, a filename, or nothing at all
where = client.download_media(message, '/path/to/output')
# Call .disconnect() when you're done
client.disconnect()
Remember that you can call ``.stringify()`` to any object Telegram returns
to pretty print it. Calling ``str(result)`` does the same operation, but on
a single line.
Available methods
*****************
This page lists all the "handy" methods available for you to use in the
``TelegramClient`` class. These are simply wrappers around the "raw"
Telegram API, making it much more manageable and easier to work with.
Please refer to :ref:`accessing-the-full-api` if these aren't enough,
and don't be afraid to read the source code of the InteractiveTelegramClient_
or even the TelegramClient_ itself to learn how it works.
.. _InteractiveTelegramClient: https://github.com/LonamiWebs/Telethon/blob/master/telethon_examples/interactive_telegram_client.py
.. _TelegramClient: https://github.com/LonamiWebs/Telethon/blob/master/telethon/telegram_client.py
.. automodule:: telethon.telegram_client
:members:
:undoc-members:
:show-inheritance:

View File

@ -14,23 +14,24 @@ The library can run in four distinguishable modes:
- With several worker threads that run your update handlers. - With several worker threads that run your update handlers.
- A mix of the above. - A mix of the above.
Since this section is about updates, we'll describe the simplest way to work with them. Since this section is about updates, we'll describe the simplest way to
work with them.
.. warning::
Remember that you should always call ``client.disconnect()`` once you're done.
Using multiple workers Using multiple workers
^^^^^^^^^^^^^^^^^^^^^^^ **********************
When you create your client, simply pass a number to the ``update_workers`` parameter: When you create your client, simply pass a number to the
``update_workers`` parameter:
``client = TelegramClient('session', api_id, api_hash, update_workers=4)`` ``client = TelegramClient('session', api_id, api_hash, update_workers=4)``
4 workers should suffice for most cases (this is also the default on `Python Telegram Bot`__). 4 workers should suffice for most cases (this is also the default on
You can set this value to more, or even less if you need. `Python Telegram Bot`__). You can set this value to more, or even less
if you need.
The next thing you want to do is to add a method that will be called when an `Update`__ arrives: The next thing you want to do is to add a method that will be called when
an `Update`__ arrives:
.. code-block:: python .. code-block:: python
@ -41,7 +42,8 @@ The next thing you want to do is to add a method that will be called when an `Up
# do more work here, or simply sleep! # do more work here, or simply sleep!
That's it! Now let's do something more interesting. That's it! Now let's do something more interesting.
Every time an user talks to use, let's reply to them with the same text reversed: Every time an user talks to use, let's reply to them with the same
text reversed:
.. code-block:: python .. code-block:: python
@ -56,16 +58,18 @@ Every time an user talks to use, let's reply to them with the same text reversed
input('Press enter to stop this!') input('Press enter to stop this!')
client.disconnect() client.disconnect()
We only ask you one thing: don't keep this running for too long, or your contacts will go mad. We only ask you one thing: don't keep this running for too long, or your
contacts will go mad.
Spawning no worker at all Spawning no worker at all
^^^^^^^^^^^^^^^^^^^^^^^^^^ *************************
All the workers do is loop forever and poll updates from a queue that is filled from the ``ReadThread``, All the workers do is loop forever and poll updates from a queue that is
responsible for reading every item off the network. filled from the ``ReadThread``, responsible for reading every item off
If you only need a worker and the ``MainThread`` would be doing no other job, the network. If you only need a worker and the ``MainThread`` would be
this is the preferred way. You can easily do the same as the workers like so: doing no other job, this is the preferred way. You can easily do the same
as the workers like so:
.. code-block:: python .. code-block:: python
@ -81,24 +85,27 @@ this is the preferred way. You can easily do the same as the workers like so:
client.disconnect() client.disconnect()
Note that ``poll`` accepts a ``timeout=`` parameter, Note that ``poll`` accepts a ``timeout=`` parameter, and it will return
and it will return ``None`` if other thread got the update before you could or if the timeout expired, ``None`` if other thread got the update before you could or if the timeout
so it's important to check ``if not update``. expired, so it's important to check ``if not update``.
This can coexist with the rest of ``N`` workers, or you can set it to ``0`` additional workers: This can coexist with the rest of ``N`` workers, or you can set it to ``0``
additional workers:
``client = TelegramClient('session', api_id, api_hash, update_workers=0)`` ``client = TelegramClient('session', api_id, api_hash, update_workers=0)``
You **must** set it to ``0`` (or other number), as it defaults to ``None`` and there is a different. You **must** set it to ``0`` (or other number), as it defaults to ``None``
``None`` workers means updates won't be processed *at all*, and there is a different. ``None`` workers means updates won't be processed
so you must set it to some value (0 or greater) if you want ``client.updates.poll()`` to work. *at all*, so you must set it to some value (``0`` or greater) if you want
``client.updates.poll()`` to work.
Using the main thread instead the ``ReadThread`` Using the main thread instead the ``ReadThread``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ************************************************
If you have no work to do on the ``MainThread`` and you were planning to have a ``while True: sleep(1)``, If you have no work to do on the ``MainThread`` and you were planning to have
don't do that. Instead, don't spawn the secondary ``ReadThread`` at all like so: a ``while True: sleep(1)``, don't do that. Instead, don't spawn the secondary
``ReadThread`` at all like so:
.. code-block:: python .. code-block:: python
@ -111,8 +118,8 @@ And then ``.idle()`` from the ``MainThread``:
``client.idle()`` ``client.idle()``
You can stop it with :kbd:`Control+C`, You can stop it with :kbd:`Control+C`, and you can configure the signals
and you can configure the signals to be used in a similar fashion to `Python Telegram Bot`__. to be used in a similar fashion to `Python Telegram Bot`__.
As a complete example: As a complete example:
@ -132,4 +139,4 @@ As a complete example:
__ https://python-telegram-bot.org/ __ https://python-telegram-bot.org/
__ https://lonamiwebs.github.io/Telethon/types/update.html __ https://lonamiwebs.github.io/Telethon/types/update.html
__ https://github.com/python-telegram-bot/python-telegram-bot/blob/4b3315db6feebafb94edcaa803df52bb49999ced/telegram/ext/updater.py#L460 __ https://github.com/python-telegram-bot/python-telegram-bot/blob/4b3315db6feebafb94edcaa803df52bb49999ced/telegram/ext/updater.py#L460

View File

@ -0,0 +1,54 @@
==========
API Status
==========
In an attempt to help everyone who works with the Telegram API, the
library will by default report all *Remote Procedure Call* errors to
`RPC PWRTelegram <https://rpc.pwrtelegram.xyz/>`__, a public database
anyone can query, made by `Daniil <https://github.com/danog>`__. All the
information sent is a ``GET`` request with the error code, error message
and method used.
If you still would like to opt out, simply set
``client.session.report_errors = False`` to disable this feature, or
pass ``report_errors=False`` as a named parameter when creating a
``TelegramClient`` instance. However Daniil would really thank you if
you helped him (and everyone) by keeping it on!
Querying the API status
***********************
The API is accessed through ``GET`` requests, which can be made for
instance through ``curl``. A JSON response will be returned.
**All known errors and their description**:
.. code:: bash
curl https://rpc.pwrtelegram.xyz/?all
**Error codes for a specific request**:
.. code:: bash
curl https://rpc.pwrtelegram.xyz/?for=messages.sendMessage
**Number of** ``RPC_CALL_FAIL``:
.. code:: bash
curl https://rpc.pwrtelegram.xyz/?rip # last hour
curl https://rpc.pwrtelegram.xyz/?rip=$(time()-60) # last minute
**Description of errors**:
.. code:: bash
curl https://rpc.pwrtelegram.xyz/?description_for=SESSION_REVOKED
**Code of a specific error**:
.. code:: bash
curl https://rpc.pwrtelegram.xyz/?code_for=STICKERSET_INVALID

View File

@ -0,0 +1,22 @@
============
Coding Style
============
Basically, make it **readable**, while keeping the style similar to the
code of whatever file you're working on.
Also note that not everyone has 4K screens for their primary monitors,
so please try to stick to the 80-columns limit. This makes it easy to
``git diff`` changes from a terminal before committing changes. If the
line has to be long, please don't exceed 120 characters.
For the commit messages, please make them *explanatory*. Not only
they're helpful to troubleshoot when certain issues could have been
introduced, but they're also used to construct the change log once a new
version is ready.
If you don't know enough Python, I strongly recommend reading `Dive Into
Python 3 <http://www.diveintopython3.net/>`__, available online for
free. For instance, remember to do ``if x is None`` or
``if x is not None`` instead ``if x == None``!

View File

@ -0,0 +1,25 @@
==========
Philosophy
==========
The intention of the library is to have an existing MTProto library
existing with hardly any dependencies (indeed, wherever Python is
available, you can run this library).
Being written in Python means that performance will be nowhere close to
other implementations written in, for instance, Java, C++, Rust, or
pretty much any other compiled language. However, the library turns out
to actually be pretty decent for common operations such as sending
messages, receiving updates, or other scripting. Uploading files may be
notably slower, but if you would like to contribute, pull requests are
appreciated!
If ``libssl`` is available on your system, the library will make use of
it to speed up some critical parts such as encrypting and decrypting the
messages. Files will notably be sent and downloaded faster.
The main focus is to keep everything clean and simple, for everyone to
understand how working with MTProto and Telegram works. Don't be afraid
to read the source, the code won't bite you! It may prove useful when
using the library on your own use cases.

View File

@ -0,0 +1,43 @@
=================
Project Structure
=================
Main interface
**************
The library itself is under the ``telethon/`` directory. The
``__init__.py`` file there exposes the main ``TelegramClient``, a class
that servers as a nice interface with the most commonly used methods on
Telegram such as sending messages, retrieving the message history,
handling updates, etc.
The ``TelegramClient`` inherits the ``TelegramBareClient``. The later is
basically a pruned version of the ``TelegramClient``, which knows basic
stuff like ``.invoke()``\ 'ing requests, downloading files, or switching
between data centers. This is primary to keep the method count per class
and file low and manageable.
Both clients make use of the ``network/mtproto_sender.py``. The
``MtProtoSender`` class handles packing requests with the ``salt``,
``id``, ``sequence``, etc., and also handles how to process responses
(i.e. pong, RPC errors). This class communicates through Telegram via
its ``.connection`` member.
The ``Connection`` class uses a ``extensions/tcp_client``, a C#-like
``TcpClient`` to ease working with sockets in Python. All the
``TcpClient`` know is how to connect through TCP and writing/reading
from the socket with optional cancel.
The ``Connection`` class bundles up all the connections modes and sends
and receives the messages accordingly (TCP full, obfuscated,
intermediate…).
Auto-generated code
*******************
The files under ``telethon_generator/`` are used to generate the code
that gets placed under ``telethon/tl/``. The ``TLGenerator`` takes in a
``.tl`` file, and spits out the generated classes which represent, as
Python classes, the request and types defined in the ``.tl`` file. It
also constructs an index so that they can be imported easily.

View File

@ -0,0 +1,64 @@
===============================
Telegram API in Other Languages
===============================
Telethon was made for **Python**, and as far as I know, there is no
*exact* port to other languages. However, there *are* other
implementations made by awesome people (one needs to be awesome to
understand the official Telegram documentation) on several languages
(even more Python too), listed below:
C
*
Possibly the most well-known unofficial open source implementation out
there by `**@vysheng** <https://github.com/vysheng>`__,
```tgl`` <https://github.com/vysheng/tgl>`__, and its console client
```telegram-cli`` <https://github.com/vysheng/tg>`__. Latest development
has been moved to `BitBucket <https://bitbucket.org/vysheng/tdcli>`__.
JavaScript
**********
`**@zerobias** <https://github.com/zerobias>`__ is working on
```telegram-mtproto`` <https://github.com/zerobias/telegram-mtproto>`__,
a work-in-progress JavaScript library installable via
```npm`` <https://www.npmjs.com/>`__.
Kotlin
******
`Kotlogram <https://github.com/badoualy/kotlogram>`__ is a Telegram
implementation written in Kotlin (the now
`official <https://blog.jetbrains.com/kotlin/2017/05/kotlin-on-android-now-official/>`__
language for
`Android <https://developer.android.com/kotlin/index.html>`__) by
`**@badoualy** <https://github.com/badoualy>`__, currently as a beta
yet working.
PHP
***
A PHP implementation is also available thanks to
`**@danog** <https://github.com/danog>`__ and his
`MadelineProto <https://github.com/danog/MadelineProto>`__ project, with
a very nice `online
documentation <https://daniil.it/MadelineProto/API_docs/>`__ too.
Python
******
A fairly new (as of the end of 2017) Telegram library written from the
ground up in Python by
`**@delivrance** <https://github.com/delivrance>`__ and his
`Pyrogram <https://github.com/pyrogram/pyrogram>`__ library! No hard
feelings Dan and good luck dealing with some of your users ;)
Rust
****
Yet another work-in-progress implementation, this time for Rust thanks
to `**@JuanPotato** <https://github.com/JuanPotato>`__ under the fancy
name of `Vail <https://github.com/JuanPotato/Vail>`__. This one is very
early still, but progress is being made at a steady rate.

View File

@ -0,0 +1,32 @@
============
Test Servers
============
To run Telethon on a test server, use the following code:
.. code-block:: python
client = TelegramClient(None, api_id, api_hash)
client.session.server_address = '149.154.167.40'
client.connect()
You can check your ``'test ip'`` on https://my.telegram.org.
You should set ``None`` session so to ensure you're generating a new
authorization key for it (it would fail if you used a session where you
had previously connected to another data center).
Once you're connected, you'll likely need to ``.sign_up()``. Remember
`anyone can access the phone you
choose <https://core.telegram.org/api/datacenter#testing-redirects>`__,
so don't store sensitive data here:
.. code-block:: python
from random import randint
dc_id = '2' # Change this to the DC id of the test server you chose
phone = '99966' + dc_id + str(randint(9999)).zfill(4)
client.send_code_request(phone)
client.sign_up(dc_id * 5, 'Some', 'Name')

View File

@ -0,0 +1,17 @@
============================
Tips for Porting the Project
============================
If you're going to use the code on this repository to guide you, please
be kind and don't forget to mention it helped you!
You should start by reading the source code on the `first
release <https://github.com/LonamiWebs/Telethon/releases/tag/v0.1>`__ of
the project, and start creating a ``MtProtoSender``. Once this is made,
you should write by hand the code to authenticate on the Telegram's
server, which are some steps required to get the key required to talk to
them. Save it somewhere! Then, simply mimic, or reinvent other parts of
the code, and it will be ready to go within a few days.
Good luck!

View File

@ -0,0 +1,35 @@
===============================
Understanding the Type Language
===============================
`Telegram's Type Language <https://core.telegram.org/mtproto/TL>`__
(also known as TL, found on ``.tl`` files) is a concise way to define
what other programming languages commonly call classes or structs.
Every definition is written as follows for a Telegram object is defined
as follows:
.. code:: tl
name#id argument_name:argument_type = CommonType
This means that in a single line you know what the ``TLObject`` name is.
You know it's unique ID, and you know what arguments it has. It really
isn't that hard to write a generator for generating code to any
platform!
The generated code should also be able to *encode* the ``TLObject`` (let
this be a request or a type) into bytes, so they can be sent over the
network. This isn't a big deal either, because you know how the
``TLObject``\ 's are made, and how the types should be serialized.
You can either write your own code generator, or use the one this
library provides, but please be kind and keep some special mention to
this project for helping you out.
This is only a introduction. The ``TL`` language is not *that* easy. But
it's not that hard either. You're free to sniff the
``telethon_generator/`` files and learn how to parse other more complex
lines, such as ``flags`` (to indicate things that may or may not be
written at all) and ``vector``\ 's.

View File

@ -1,13 +1,14 @@
====== ====
Bots Bots
====== ====
Talking to Inline Bots Talking to Inline Bots
^^^^^^^^^^^^^^^^^^^^^^ **********************
You can query an inline bot, such as `@VoteBot`__ You can query an inline bot, such as `@VoteBot`__ (note, *query*,
(note, *query*, not *interact* with a voting message), by making use of not *interact* with a voting message), by making use of the
the `GetInlineBotResultsRequest`__ request: `GetInlineBotResultsRequest`__ request:
.. code-block:: python .. code-block:: python
@ -32,11 +33,10 @@ And you can select any of their results by using
Talking to Bots with special reply markup Talking to Bots with special reply markup
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ *****************************************
To interact with a message that has a special reply markup, such as To interact with a message that has a special reply markup, such as
`@VoteBot`__ polls, you would use `@VoteBot`__ polls, you would use `GetBotCallbackAnswerRequest`__:
`GetBotCallbackAnswerRequest`__:
.. code-block:: python .. code-block:: python
@ -48,7 +48,7 @@ To interact with a message that has a special reply markup, such as
data=msg.reply_markup.rows[wanted_row].buttons[wanted_button].data data=msg.reply_markup.rows[wanted_row].buttons[wanted_button].data
)) ))
Its a bit verbose, but it has all the information you would need to It's a bit verbose, but it has all the information you would need to
show it visually (button rows, and buttons within each row, each with show it visually (button rows, and buttons within each row, each with
its own data). its own data).
@ -56,4 +56,4 @@ __ https://t.me/vote
__ https://lonamiwebs.github.io/Telethon/methods/messages/get_inline_bot_results.html __ https://lonamiwebs.github.io/Telethon/methods/messages/get_inline_bot_results.html
__ https://lonamiwebs.github.io/Telethon/methods/messages/send_inline_bot_result.html __ https://lonamiwebs.github.io/Telethon/methods/messages/send_inline_bot_result.html
__ https://lonamiwebs.github.io/Telethon/methods/messages/get_bot_callback_answer.html __ https://lonamiwebs.github.io/Telethon/methods/messages/get_bot_callback_answer.html
__ https://t.me/vote __ https://t.me/vote

View File

@ -0,0 +1,205 @@
===============================
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``.
Joining a public channel
************************
Once you have the :ref:`entity <entities>` of the channel you want to join
to, you can make use of the `JoinChannelRequest`__ to join such channel:
.. code-block:: python
from telethon.tl.functions.channels import JoinChannelRequest
client(JoinChannelRequest(channel))
# In the same way, you can also leave such channel
from telethon.tl.functions.channels import LeaveChannelRequest
client(LeaveChannelRequest(input_channel))
For more on channels, check the `channels namespace`__.
Joining a private chat or channel
*********************************
If all you have is a link like this one:
``https://t.me/joinchat/AAAAAFFszQPyPEZ7wgxLtd``, you already have
enough information to join! The part after the
``https://t.me/joinchat/``, this is, ``AAAAAFFszQPyPEZ7wgxLtd`` on this
example, is the ``hash`` of the chat or channel. Now you can use
`ImportChatInviteRequest`__ as follows:
.. -block:: python
from telethon.tl.functions.messages import ImportChatInviteRequest
updates = client(ImportChatInviteRequest('AAAAAEHbEkejzxUjAUCfYg'))
Adding someone else to such chat or channel
*******************************************
If you don't want to add yourself, maybe because you're already in,
you can always add someone else with the `AddChatUserRequest`__,
which use is very straightforward:
.. code-block:: python
from telethon.tl.functions.messages import AddChatUserRequest
client(AddChatUserRequest(
chat_id,
user_to_add,
fwd_limit=10 # allow the user to see the 10 last messages
))
Checking a link without joining
*******************************
If you don't need to join but rather check whether it's a group or a
channel, you can use the `CheckChatInviteRequest`__, which takes in
the `hash`__ of said channel or group.
__ https://lonamiwebs.github.io/Telethon/constructors/chat.html
__ https://lonamiwebs.github.io/Telethon/constructors/channel.html
__ https://lonamiwebs.github.io/Telethon/types/chat.html
__ https://lonamiwebs.github.io/Telethon/methods/channels/join_channel.html
__ https://lonamiwebs.github.io/Telethon/methods/channels/index.html
__ https://lonamiwebs.github.io/Telethon/methods/messages/import_chat_invite.html
__ https://lonamiwebs.github.io/Telethon/methods/messages/add_chat_user.html
__ https://lonamiwebs.github.io/Telethon/methods/messages/check_chat_invite.html
__ https://github.com/LonamiWebs/Telethon/wiki/Joining-a-chat-or-channel#joining-a-private-chat-or-channel
Retrieving all chat members (channels too)
******************************************
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
use will work), and a mandatory `ChannelParticipantsFilter`__. The
closest thing to "no filter" is to simply use
`ChannelParticipantsSearch`__ with an empty ``'q'`` string.
If we want to get *all* the members, we need to use a moving offset and
a fixed limit:
.. code-block:: python
from telethon.tl.functions.channels import GetParticipantsRequest
from telethon.tl.types import ChannelParticipantsSearch
from time import sleep
offset = 0
limit = 100
all_participants = []
while True:
participants = client.invoke(GetParticipantsRequest(
channel, ChannelParticipantsSearch(''), offset, limit
))
if not participants.users:
break
all_participants.extend(participants.users)
offset += len(participants.users)
Note that ``GetParticipantsRequest`` returns `ChannelParticipants`__,
which may have more information you need (like the role of the
participants, total count of members, etc.)
__ 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
Recent Actions
**************
"Recent actions" is simply the name official applications have given to
the "admin log". Simply use `GetAdminLogRequest`__ for that, and
you'll get AdminLogResults.events in return which in turn has the final
`.action`__.
__ https://lonamiwebs.github.io/Telethon/methods/channels/get_admin_log.html
__ https://lonamiwebs.github.io/Telethon/types/channel_admin_log_event_action.html
Admin Permissions
*****************
Giving or revoking admin permissions can be done with the `EditAdminRequest`__:
.. code-block:: python
from telethon.tl.functions.channels import EditAdminRequest
from telethon.tl.types import ChannelAdminRights
# You need both the channel and who to grant permissions
# They can either be channel/user or input channel/input user.
#
# ChannelAdminRights is a list of granted permissions.
# Set to True those you want to give.
rights = ChannelAdminRights(
post_messages=None,
add_admins=None,
invite_users=None,
change_info=True,
ban_users=None,
delete_messages=True,
pin_messages=True,
invite_link=None,
edit_messages=None
)
client(EditAdminRequest(channel, who, rights))
Thanks to `@Kyle2142`__ for `pointing out`__ that you **cannot** set
to ``True`` the ``post_messages`` and ``edit_messages`` fields. Those that
are ``None`` can be omitted (left here so you know `which are available`__.
__ https://lonamiwebs.github.io/Telethon/methods/channels/edit_admin.html
__ https://github.com/Kyle2142
__ https://github.com/LonamiWebs/Telethon/issues/490
__ https://lonamiwebs.github.io/Telethon/constructors/channel_admin_rights.html
Increasing View Count in a Channel
**********************************
It has been asked `quite`__ `a few`__ `times`__ (really, `many`__), and
while I don't understand why so many people ask this, the solution is to
use `GetMessagesViewsRequest`__, setting ``increment=True``:
.. code-block:: python
# Obtain `channel' through dialogs or through client.get_entity() or anyhow.
# Obtain `msg_ids' through `.get_message_history()` or anyhow. Must be a list.
client(GetMessagesViewsRequest(
peer=channel,
id=msg_ids,
increment=True
))
__ https://github.com/LonamiWebs/Telethon/issues/233
__ https://github.com/LonamiWebs/Telethon/issues/305
__ https://github.com/LonamiWebs/Telethon/issues/409
__ https://github.com/LonamiWebs/Telethon/issues/447
__ https://lonamiwebs.github.io/Telethon/methods/messages/get_messages_views.html

View File

@ -1,20 +1,18 @@
========================= =====================
Working with messages Working with messages
========================= =====================
.. note::
Make sure you have gone through :ref:`prelude` already!
Forwarding messages Forwarding messages
******************* *******************
Note that ForwardMessageRequest_ (note it's Message, singular) will *not* work if channels are involved. Note that ForwardMessageRequest_ (note it's Message, singular) will *not*
This is because channel (and megagroups) IDs are not unique, so you also need to know who the sender is work if channels are involved. This is because channel (and megagroups) IDs
(a parameter this request doesn't have). 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*, Either way, you are encouraged to use ForwardMessagesRequest_ (note it's
since it is more powerful, as follows: Message*s*, plural) *always*, since it is more powerful, as follows:
.. code-block:: python .. code-block:: python
@ -31,14 +29,16 @@ since it is more powerful, as follows:
to_peer=to_entity # who are we forwarding them to? to_peer=to_entity # who are we forwarding them to?
)) ))
The named arguments are there for clarity, although they're not needed because they appear in order. The named arguments are there for clarity, although they're not needed because
You can obviously just wrap a single message on the list too, if that's all you have. they appear in order. You can obviously just wrap a single message on the list
too, if that's all you have.
Searching Messages Searching Messages
******************* *******************
Messages are searched through the obvious SearchRequest_, but you may run into issues_. A valid example would be: Messages are searched through the obvious SearchRequest_, but you may run
into issues_. A valid example would be:
.. code-block:: python .. code-block:: python
@ -46,27 +46,32 @@ Messages are searched through the obvious SearchRequest_, but you may run into i
entity, 'query', InputMessagesFilterEmpty(), None, None, 0, 0, 100 entity, 'query', InputMessagesFilterEmpty(), None, None, 0, 0, 100
)) ))
It's important to note that the optional parameter ``from_id`` has been left omitted and thus defaults to ``None``. It's important to note that the optional parameter ``from_id`` has been left
Changing it to InputUserEmpty_, as one could think to specify "no user", won't work because this parameter is a flag, omitted and thus defaults to ``None``. Changing it to InputUserEmpty_, as one
could think to specify "no user", won't work because this parameter is a flag,
and it being unspecified has a different meaning. and it being unspecified has a different meaning.
If one were to set ``from_id=InputUserEmpty()``, it would filter messages from "empty" senders, If one were to set ``from_id=InputUserEmpty()``, it would filter messages
which would likely match no users. from "empty" senders, which would likely match no users.
If you get a ``ChatAdminRequiredError`` on a channel, it's probably because you tried setting the ``from_id`` filter, If you get a ``ChatAdminRequiredError`` on a channel, it's probably because
and as the error says, you can't do that. Leave it set to ``None`` and it should work. you tried setting the ``from_id`` filter, and as the error says, you can't
do that. Leave it set to ``None`` and it should work.
As with every method, make sure you use the right ID/hash combination for your ``InputUser`` or ``InputChat``, As with every method, make sure you use the right ID/hash combination for
or you'll likely run into errors like ``UserIdInvalidError``. your ``InputUser`` or ``InputChat``, or you'll likely run into errors like
``UserIdInvalidError``.
Sending stickers Sending stickers
***************** ****************
Stickers are nothing else than ``files``, and when you successfully retrieve the stickers for a certain sticker set, Stickers are nothing else than ``files``, and when you successfully retrieve
all you will have are ``handles`` to these files. Remember, the files Telegram holds on their servers can be referenced the stickers for a certain sticker set, all you will have are ``handles`` to
through this pair of ID/hash (unique per user), and you need to use this handle when sending a "document" message. these files. Remember, the files Telegram holds on their servers can be
This working example will send yourself the very first sticker you have: referenced through this pair of ID/hash (unique per user), and you need to
use this handle when sending a "document" message. This working example will
send yourself the very first sticker you have:
.. code-block:: python .. code-block:: python

View File

@ -1,6 +1,6 @@
========================================= ========================================
Deleted, Limited or Deactivated Accounts Deleted, Limited or Deactivated Accounts
========================================= ========================================
If you're from Iran or Russian, we have bad news for you. If you're from Iran or Russian, we have bad news for you.
Telegram is much more likely to ban these numbers, Telegram is much more likely to ban these numbers,
@ -23,4 +23,4 @@ For more discussion, please see `issue 297`__.
__ https://t.me/SpamBot __ https://t.me/SpamBot
__ https://github.com/LonamiWebs/Telethon/issues/297 __ https://github.com/LonamiWebs/Telethon/issues/297

View File

@ -1,15 +1,18 @@
================ ================
Enable Logging Enabling Logging
================ ================
Telethon makes use of the `logging`__ module, and you can enable it as follows: Telethon makes use of the `logging`__ module, and you can enable it as follows:
.. code-block:: python .. code:: python
import logging import logging
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
You can also use it in your own project very easily: The library has the `NullHandler`__ added by default so that no log calls
will be printed unless you explicitly enable it.
You can also `use the module`__ on your own project very easily:
.. code-block:: python .. code-block:: python
@ -21,4 +24,17 @@ You can also use it in your own project very easily:
logger.warning('This is a warning!') logger.warning('This is a warning!')
__ https://docs.python.org/3/library/logging.html If you want to enable ``logging`` for your project *but* use a different
log level for the library:
.. code-block:: python
import logging
logging.basicConfig(level=logging.DEBUG)
# For instance, show only warnings and above
logging.getLogger('telethon').setLevel(level=logging.WARNING)
__ https://docs.python.org/3/library/logging.html
__ https://docs.python.org/3/howto/logging.html#configuring-logging-for-a-library
__ https://docs.python.org/3/howto/logging.html

View File

@ -3,17 +3,17 @@ RPC Errors
========== ==========
RPC stands for Remote Procedure Call, and when Telethon raises an RPC stands for Remote Procedure Call, and when Telethon raises an
``RPCError``, its most likely because you have invoked some of the API ``RPCError``, it's most likely because you have invoked some of the API
methods incorrectly (wrong parameters, wrong permissions, or even methods incorrectly (wrong parameters, wrong permissions, or even
something went wrong on Telegrams server). The most common are: something went wrong on Telegram's server). The most common are:
- ``FloodError`` (420), the same request was repeated many times. Must - ``FloodWaitError`` (420), the same request was repeated many times.
wait ``.seconds``. Must wait ``.seconds`` (you can access this parameter).
- ``SessionPasswordNeededError``, if you have setup two-steps - ``SessionPasswordNeededError``, if you have setup two-steps
verification on Telegram. verification on Telegram.
- ``CdnFileTamperedError``, if the media you were trying to download - ``CdnFileTamperedError``, if the media you were trying to download
from a CDN has been altered. from a CDN has been altered.
- ``ChatAdminRequiredError``, you dont have permissions to perform - ``ChatAdminRequiredError``, you don't have permissions to perform
said operation on a chat or channel. Try avoiding filters, i.e. when said operation on a chat or channel. Try avoiding filters, i.e. when
searching messages. searching messages.
@ -22,6 +22,6 @@ The generic classes for different error codes are: \* ``InvalidDCError``
``BadRequestError`` (400), the request contained errors. \* ``BadRequestError`` (400), the request contained errors. \*
``UnauthorizedError`` (401), the user is not authorized yet. \* ``UnauthorizedError`` (401), the user is not authorized yet. \*
``ForbiddenError`` (403), privacy violation error. \* ``NotFoundError`` ``ForbiddenError`` (403), privacy violation error. \* ``NotFoundError``
(404), make sure youre invoking ``Request``\ s! (404), make sure you're invoking ``Request``\ 's!
If the error is not recognised, it will only be an ``RPCError``. If the error is not recognised, it will only be an ``RPCError``.

View File

@ -0,0 +1,62 @@
=============
Wall of Shame
=============
This project has an
`issues <https://github.com/LonamiWebs/Telethon/issues>`__ section for
you to file **issues** whenever you encounter any when working with the
library. Said section is **not** for issues on *your* program but rather
issues with Telethon itself.
If you have not made the effort to 1. `read through the
wiki <https://github.com/LonamiWebs/Telethon/wiki>`__ and 2. `look for
the method you need <https://lonamiwebs.github.io/Telethon/>`__, you
will end up on the `Wall of
Shame <https://github.com/LonamiWebs/Telethon/issues?q=is%3Aissue+label%3ARTFM+is%3Aclosed>`__,
i.e. all issues labeled
`"RTFM" <http://www.urbandictionary.com/define.php?term=RTFM>`__:
> > **rtfm**
> > Literally "Read The F\ **king Manual"; a term showing the
frustration of being bothered with questions so trivial that the asker
could have quickly figured out the answer on their own with minimal
effort, usually by reading readily-available documents. People who
say"RTFM!" might be considered rude, but the true rude ones are the
annoying people who take absolutely no self-responibility and expect to
have all the answers handed to them personally.
> > *"Damn, that's the twelveth time that somebody posted this question
to the messageboard today! RTFM, already!"*
> > **\ by Bill M. July 27, 2004*\*
If you have indeed read the wiki, and have tried looking for the method,
and yet you didn't find what you need, **that's fine**. Telegram's API
can have some obscure names at times, and for this reason, there is a
`"question"
label <https://github.com/LonamiWebs/Telethon/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20label%3Aquestion%20>`__
with questions that are okay to ask. Just state what you've tried so
that we know you've made an effort, or you'll go to the Wall of Shame.
Of course, if the issue you're going to open is not even a question but
a real issue with the library (thankfully, most of the issues have been
that!), you won't end up here. Don't worry.
Current winner
--------------
The current winner is `issue
213 <https://github.com/LonamiWebs/Telethon/issues/213>`__:
**Issue:**
.. figure:: https://user-images.githubusercontent.com/6297805/29822978-9a9a6ef0-8ccd-11e7-9ec5-934ea0f57681.jpg
:alt: Winner issue
Winner issue
**Answer:**
.. figure:: https://user-images.githubusercontent.com/6297805/29822983-9d523402-8ccd-11e7-9fb1-5783740ee366.jpg
:alt: Winner issue answer
Winner issue answer

View File

@ -3,11 +3,14 @@
You can adapt this file completely to your liking, but it should at least You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive. contain the root `toctree` directive.
====================================
Welcome to Telethon's documentation! Welcome to Telethon's documentation!
==================================== ====================================
Pure Python 3 Telegram client library. Official Site `here <https://lonamiwebs.github.io/Telethon>`_.
Pure Python 3 Telegram client library.
Official Site `here <https://lonamiwebs.github.io/Telethon>`_.
Please follow the links below to get you started.
.. _installation-and-usage: .. _installation-and-usage:
@ -19,10 +22,9 @@ Pure Python 3 Telegram client library. Official Site `here <https://lonamiwebs.g
extra/basic/getting-started extra/basic/getting-started
extra/basic/installation extra/basic/installation
extra/basic/creating-a-client extra/basic/creating-a-client
extra/basic/sessions extra/basic/telegram-client
extra/basic/sending-requests extra/basic/entities
extra/basic/working-with-updates extra/basic/working-with-updates
extra/basic/accessing-the-full-api
.. _Advanced-usage: .. _Advanced-usage:
@ -31,11 +33,19 @@ Pure Python 3 Telegram client library. Official Site `here <https://lonamiwebs.g
:maxdepth: 2 :maxdepth: 2
:caption: Advanced Usage :caption: Advanced Usage
extra/advanced extra/advanced-usage/accessing-the-full-api
extra/advanced-usage/signing-in extra/advanced-usage/sessions
extra/advanced-usage/working-with-messages
extra/advanced-usage/users-and-chats
extra/advanced-usage/bots .. _Examples:
.. toctree::
:maxdepth: 2
:caption: Examples
extra/examples/working-with-messages
extra/examples/chats-and-channels
extra/examples/bots
.. _Troubleshooting: .. _Troubleshooting:
@ -49,6 +59,30 @@ Pure Python 3 Telegram client library. Official Site `here <https://lonamiwebs.g
extra/troubleshooting/rpc-errors extra/troubleshooting/rpc-errors
.. _Developing:
.. toctree::
:maxdepth: 2
:caption: Developing
extra/developing/philosophy.rst
extra/developing/api-status.rst
extra/developing/test-servers.rst
extra/developing/project-structure.rst
extra/developing/coding-style.rst
extra/developing/understanding-the-type-language.rst
extra/developing/tips-for-porting-the-project.rst
extra/developing/telegram-api-in-other-languages.rst
.. _Wall-of-shame:
.. toctree::
:maxdepth: 2
:caption: Wall of Shame
extra/wall-of-shame.rst
.. toctree:: .. toctree::
:caption: Telethon modules :caption: Telethon modules
@ -56,7 +90,6 @@ Pure Python 3 Telegram client library. Official Site `here <https://lonamiwebs.g
telethon telethon
Indices and tables Indices and tables
================== ==================

View File

@ -71,6 +71,16 @@ def main():
print('Done.') print('Done.')
elif len(argv) >= 2 and argv[1] == 'pypi': elif len(argv) >= 2 and argv[1] == 'pypi':
# (Re)generate the code to make sure we don't push without it
gen_tl()
# Try importing the telethon module to assert it has no errors
try:
import telethon
except:
print('Packaging for PyPi aborted, importing the module failed.')
return
# Need python3.5 or higher, but Telethon is supposed to support 3.x # Need python3.5 or higher, but Telethon is supposed to support 3.x
# Place it here since noone should be running ./setup.py pypi anyway # Place it here since noone should be running ./setup.py pypi anyway
from subprocess import run from subprocess import run

View File

@ -1,3 +1,8 @@
"""
This module contains several utilities regarding cryptographic purposes,
such as the AES IGE mode used by Telegram, the authorization key bound with
their data centers, and so on.
"""
from .aes import AES from .aes import AES
from .aes_ctr import AESModeCTR from .aes_ctr import AESModeCTR
from .auth_key import AuthKey from .auth_key import AuthKey

View File

@ -1,3 +1,6 @@
"""
AES IGE implementation in Python. This module may use libssl if available.
"""
import os import os
import pyaes import pyaes
from . import libssl from . import libssl
@ -9,10 +12,15 @@ if libssl.AES is not None:
else: else:
# Fallback to a pure Python implementation # Fallback to a pure Python implementation
class AES: class AES:
"""
Class that servers as an interface to encrypt and decrypt
text through the AES IGE mode.
"""
@staticmethod @staticmethod
def decrypt_ige(cipher_text, key, iv): def decrypt_ige(cipher_text, key, iv):
"""Decrypts the given text in 16-bytes blocks by using the """
given key and 32-bytes initialization vector Decrypts the given text in 16-bytes blocks by using the
given key and 32-bytes initialization vector.
""" """
iv1 = iv[:len(iv) // 2] iv1 = iv[:len(iv) // 2]
iv2 = iv[len(iv) // 2:] iv2 = iv[len(iv) // 2:]
@ -42,8 +50,9 @@ else:
@staticmethod @staticmethod
def encrypt_ige(plain_text, key, iv): def encrypt_ige(plain_text, key, iv):
"""Encrypts the given text in 16-bytes blocks by using the """
given key and 32-bytes initialization vector Encrypts the given text in 16-bytes blocks by using the
given key and 32-bytes initialization vector.
""" """
# Add random padding iff it's not evenly divisible by 16 already # Add random padding iff it's not evenly divisible by 16 already

View File

@ -1,3 +1,6 @@
"""
This module holds the AESModeCTR wrapper class.
"""
import pyaes import pyaes
@ -6,6 +9,12 @@ class AESModeCTR:
# TODO Maybe make a pull request to pyaes to support iv on CTR # TODO Maybe make a pull request to pyaes to support iv on CTR
def __init__(self, key, iv): def __init__(self, key, iv):
"""
Initializes the AES CTR mode with the given key/iv pair.
:param key: the key to be used as bytes.
:param iv: the bytes initialization vector. Must have a length of 16.
"""
# TODO Use libssl if available # TODO Use libssl if available
assert isinstance(key, bytes) assert isinstance(key, bytes)
self._aes = pyaes.AESModeOfOperationCTR(key) self._aes = pyaes.AESModeOfOperationCTR(key)
@ -15,7 +24,19 @@ class AESModeCTR:
self._aes._counter._counter = list(iv) self._aes._counter._counter = list(iv)
def encrypt(self, data): def encrypt(self, data):
"""
Encrypts the given plain text through AES CTR.
:param data: the plain text to be encrypted.
:return: the encrypted cipher text.
"""
return self._aes.encrypt(data) return self._aes.encrypt(data)
def decrypt(self, data): def decrypt(self, data):
"""
Decrypts the given cipher text through AES CTR
:param data: the cipher text to be decrypted.
:return: the decrypted plain text.
"""
return self._aes.decrypt(data) return self._aes.decrypt(data)

View File

@ -1,3 +1,6 @@
"""
This module holds the AuthKey class.
"""
import struct import struct
from hashlib import sha1 from hashlib import sha1
@ -6,7 +9,16 @@ from ..extensions import BinaryReader
class AuthKey: class AuthKey:
"""
Represents an authorization key, used to encrypt and decrypt
messages sent to Telegram's data centers.
"""
def __init__(self, data): def __init__(self, data):
"""
Initializes a new authorization key.
:param data: the data in bytes that represent this auth key.
"""
self.key = data self.key = data
with BinaryReader(sha1(self.key).digest()) as reader: with BinaryReader(sha1(self.key).digest()) as reader:
@ -15,8 +27,12 @@ class AuthKey:
self.key_id = reader.read_long(signed=False) self.key_id = reader.read_long(signed=False)
def calc_new_nonce_hash(self, new_nonce, number): def calc_new_nonce_hash(self, new_nonce, number):
"""Calculates the new nonce hash based on """
the current class fields' values Calculates the new nonce hash based on the current attributes.
:param new_nonce: the new nonce to be hashed.
:param number: number to prepend before the hash.
:return: the hash for the given new nonce.
""" """
new_nonce = new_nonce.to_bytes(32, 'little', signed=True) new_nonce = new_nonce.to_bytes(32, 'little', signed=True)
data = new_nonce + struct.pack('<BQ', number, self.aux_hash) data = new_nonce + struct.pack('<BQ', number, self.aux_hash)

View File

@ -1,6 +1,8 @@
"""
This module holds the CdnDecrypter utility class.
"""
from hashlib import sha256 from hashlib import sha256
from ..tl import Session
from ..tl.functions.upload import GetCdnFileRequest, ReuploadCdnFileRequest from ..tl.functions.upload import GetCdnFileRequest, ReuploadCdnFileRequest
from ..tl.types.upload import CdnFileReuploadNeeded, CdnFile from ..tl.types.upload import CdnFileReuploadNeeded, CdnFile
from ..crypto import AESModeCTR from ..crypto import AESModeCTR
@ -8,11 +10,20 @@ from ..errors import CdnFileTamperedError
class CdnDecrypter: class CdnDecrypter:
"""Used when downloading a file results in a 'FileCdnRedirect' to """
both prepare the redirect, decrypt the file as it downloads, and Used when downloading a file results in a 'FileCdnRedirect' to
ensure the file hasn't been tampered. https://core.telegram.org/cdn both prepare the redirect, decrypt the file as it downloads, and
ensure the file hasn't been tampered. https://core.telegram.org/cdn
""" """
def __init__(self, cdn_client, file_token, cdn_aes, cdn_file_hashes): def __init__(self, cdn_client, file_token, cdn_aes, cdn_file_hashes):
"""
Initializes the CDN decrypter.
:param cdn_client: a client connected to a CDN.
:param file_token: the token of the file to be used.
:param cdn_aes: the AES CTR used to decrypt the file.
:param cdn_file_hashes: the hashes the decrypted file must match.
"""
self.client = cdn_client self.client = cdn_client
self.file_token = file_token self.file_token = file_token
self.cdn_aes = cdn_aes self.cdn_aes = cdn_aes
@ -20,10 +31,13 @@ class CdnDecrypter:
@staticmethod @staticmethod
async def prepare_decrypter(client, cdn_client, cdn_redirect): async def prepare_decrypter(client, cdn_client, cdn_redirect):
"""Prepares a CDN decrypter, returning (decrypter, file data). """
'client' should be an existing client not connected to a CDN. Prepares a new CDN decrypter.
'cdn_client' should be an already-connected TelegramBareClient
with the auth key already created. :param client: a TelegramClient connected to the main servers.
:param cdn_client: a new client connected to the CDN.
:param cdn_redirect: the redirect file object that caused this call.
:return: (CdnDecrypter, first chunk file data)
""" """
cdn_aes = AESModeCTR( cdn_aes = AESModeCTR(
key=cdn_redirect.encryption_key, key=cdn_redirect.encryption_key,
@ -60,8 +74,11 @@ class CdnDecrypter:
return decrypter, cdn_file return decrypter, cdn_file
async def get_file(self): async def get_file(self):
"""Calls GetCdnFileRequest and decrypts its bytes. """
Also ensures that the file hasn't been tampered. Calls GetCdnFileRequest and decrypts its bytes.
Also ensures that the file hasn't been tampered.
:return: the CdnFile result.
""" """
if self.cdn_file_hashes: if self.cdn_file_hashes:
cdn_hash = self.cdn_file_hashes.pop(0) cdn_hash = self.cdn_file_hashes.pop(0)
@ -77,6 +94,12 @@ class CdnDecrypter:
@staticmethod @staticmethod
def check(data, cdn_hash): def check(data, cdn_hash):
"""Checks the integrity of the given data""" """
Checks the integrity of the given data.
Raises CdnFileTamperedError if the integrity check fails.
:param data: the data to be hashed.
:param cdn_hash: the expected hash.
"""
if sha256(data).digest() != cdn_hash.hash: if sha256(data).digest() != cdn_hash.hash:
raise CdnFileTamperedError() raise CdnFileTamperedError()

View File

@ -1,9 +1,21 @@
"""
This module holds a fast Factorization class.
"""
from random import randint from random import randint
class Factorization: class Factorization:
"""
Simple module to factorize large numbers really quickly.
"""
@classmethod @classmethod
def factorize(cls, pq): def factorize(cls, pq):
"""
Factorizes the given large integer.
:param pq: the prime pair pq.
:return: a tuple containing the two factors p and q.
"""
if pq % 2 == 0: if pq % 2 == 0:
return 2, pq // 2 return 2, pq // 2
@ -39,6 +51,13 @@ class Factorization:
@staticmethod @staticmethod
def gcd(a, b): def gcd(a, b):
"""
Calculates the Greatest Common Divisor.
:param a: the first number.
:param b: the second number.
:return: GCD(a, b)
"""
while b: while b:
a, b = b, a % b a, b = b, a % b

View File

@ -1,3 +1,6 @@
"""
This module holds an AES IGE class, if libssl is available on the system.
"""
import os import os
import ctypes import ctypes
from ctypes.util import find_library from ctypes.util import find_library
@ -35,14 +38,23 @@ else:
AES_DECRYPT = ctypes.c_int(0) AES_DECRYPT = ctypes.c_int(0)
class AES_KEY(ctypes.Structure): class AES_KEY(ctypes.Structure):
"""Helper class representing an AES key"""
_fields_ = [ _fields_ = [
('rd_key', ctypes.c_uint32 * (4*(AES_MAXNR + 1))), ('rd_key', ctypes.c_uint32 * (4*(AES_MAXNR + 1))),
('rounds', ctypes.c_uint), ('rounds', ctypes.c_uint),
] ]
class AES: class AES:
"""
Class that servers as an interface to encrypt and decrypt
text through the AES IGE mode, using the system's libssl.
"""
@staticmethod @staticmethod
def decrypt_ige(cipher_text, key, iv): def decrypt_ige(cipher_text, key, iv):
"""
Decrypts the given text in 16-bytes blocks by using the
given key and 32-bytes initialization vector.
"""
aeskey = AES_KEY() aeskey = AES_KEY()
ckey = (ctypes.c_ubyte * len(key))(*key) ckey = (ctypes.c_ubyte * len(key))(*key)
cklen = ctypes.c_int(len(key)*8) cklen = ctypes.c_int(len(key)*8)
@ -65,6 +77,10 @@ else:
@staticmethod @staticmethod
def encrypt_ige(plain_text, key, iv): def encrypt_ige(plain_text, key, iv):
"""
Encrypts the given text in 16-bytes blocks by using the
given key and 32-bytes initialization vector.
"""
# Add random padding iff it's not evenly divisible by 16 already # Add random padding iff it's not evenly divisible by 16 already
if len(plain_text) % 16 != 0: if len(plain_text) % 16 != 0:
padding_count = 16 - len(plain_text) % 16 padding_count = 16 - len(plain_text) % 16

View File

@ -1,3 +1,6 @@
"""
This module holds several utilities regarding RSA and server fingerprints.
"""
import os import os
import struct import struct
from hashlib import sha1 from hashlib import sha1
@ -32,8 +35,11 @@ def get_byte_array(integer):
def _compute_fingerprint(key): def _compute_fingerprint(key):
"""For a given Crypto.RSA key, computes its 8-bytes-long fingerprint """
in the same way that Telegram does. Given a RSA key, computes its fingerprint like Telegram does.
:param key: the Crypto.RSA key.
:return: its 8-bytes-long fingerprint.
""" """
n = TLObject.serialize_bytes(get_byte_array(key.n)) n = TLObject.serialize_bytes(get_byte_array(key.n))
e = TLObject.serialize_bytes(get_byte_array(key.e)) e = TLObject.serialize_bytes(get_byte_array(key.e))
@ -49,8 +55,14 @@ def add_key(pub):
def encrypt(fingerprint, data): def encrypt(fingerprint, data):
"""Given the fingerprint of a previously added RSA key, encrypt its data """
in the way Telegram requires us to do so (sha1(data) + data + padding) Encrypts the given data known the fingerprint to be used
in the way Telegram requires us to do so (sha1(data) + data + padding)
:param fingerprint: the fingerprint of the RSA key.
:param data: the data to be encrypted.
:return:
the cipher text, or None if no key matching this fingerprint is found.
""" """
global _server_keys global _server_keys
key = _server_keys.get(fingerprint, None) key = _server_keys.get(fingerprint, None)

View File

@ -1,11 +1,14 @@
"""
This module holds all the base and automatically generated errors that the
Telegram API has. See telethon_generator/errors.json for more.
"""
import urllib.request import urllib.request
import re import re
from threading import Thread from threading import Thread
from .common import ( from .common import (
ReadCancelledError, InvalidParameterError, TypeNotFoundError, ReadCancelledError, TypeNotFoundError, InvalidChecksumError,
InvalidChecksumError, BrokenAuthKeyError, SecurityError, BrokenAuthKeyError, SecurityError, CdnFileTamperedError
CdnFileTamperedError
) )
# This imports the base errors too, as they're imported there # This imports the base errors too, as they're imported there
@ -13,6 +16,13 @@ from .rpc_error_list import *
def report_error(code, message, report_method): def report_error(code, message, report_method):
"""
Reports an RPC error to pwrtelegram.
:param code: the integer code of the error (like 400).
:param message: the message representing the error.
:param report_method: the constructor ID of the function that caused it.
"""
try: try:
# Ensure it's signed # Ensure it's signed
report_method = int.from_bytes( report_method = int.from_bytes(
@ -30,6 +40,14 @@ def report_error(code, message, report_method):
def rpc_message_to_error(code, message, report_method=None): def rpc_message_to_error(code, message, report_method=None):
"""
Converts a Telegram's RPC Error to a Python error.
:param code: the integer code of the error (like 400).
:param message: the message representing the error.
:param report_method: if present, the ID of the method that caused it.
:return: the RPCError as a Python exception that represents this error.
"""
if report_method is not None: if report_method is not None:
Thread( Thread(
target=report_error, target=report_error,

View File

@ -2,20 +2,16 @@
class ReadCancelledError(Exception): class ReadCancelledError(Exception):
"""Occurs when a read operation was cancelled""" """Occurs when a read operation was cancelled."""
def __init__(self): def __init__(self):
super().__init__(self, 'The read operation was cancelled.') super().__init__(self, 'The read operation was cancelled.')
class InvalidParameterError(Exception):
"""Occurs when an invalid parameter is given, for example,
when either A or B are required but none is given"""
class TypeNotFoundError(Exception): class TypeNotFoundError(Exception):
"""Occurs when a type is not found, for example, """
when trying to read a TLObject with an invalid constructor code""" Occurs when a type is not found, for example,
when trying to read a TLObject with an invalid constructor code.
"""
def __init__(self, invalid_constructor_id): def __init__(self, invalid_constructor_id):
super().__init__( super().__init__(
self, 'Could not find a matching Constructor ID for the TLObject ' self, 'Could not find a matching Constructor ID for the TLObject '
@ -27,6 +23,10 @@ class TypeNotFoundError(Exception):
class InvalidChecksumError(Exception): class InvalidChecksumError(Exception):
"""
Occurs when using the TCP full mode and the checksum of a received
packet doesn't match the expected checksum.
"""
def __init__(self, checksum, valid_checksum): def __init__(self, checksum, valid_checksum):
super().__init__( super().__init__(
self, self,
@ -39,6 +39,9 @@ class InvalidChecksumError(Exception):
class BrokenAuthKeyError(Exception): class BrokenAuthKeyError(Exception):
"""
Occurs when the authorization key for a data center is not valid.
"""
def __init__(self): def __init__(self):
super().__init__( super().__init__(
self, self,
@ -47,6 +50,9 @@ class BrokenAuthKeyError(Exception):
class SecurityError(Exception): class SecurityError(Exception):
"""
Generic security error, mostly used when generating a new AuthKey.
"""
def __init__(self, *args): def __init__(self, *args):
if not args: if not args:
args = ['A security check failed.'] args = ['A security check failed.']
@ -54,6 +60,10 @@ class SecurityError(Exception):
class CdnFileTamperedError(SecurityError): class CdnFileTamperedError(SecurityError):
"""
Occurs when there's a hash mismatch between the decrypted CDN file
and its expected hash.
"""
def __init__(self): def __init__(self):
super().__init__( super().__init__(
'The CDN file has been altered and its download cancelled.' 'The CDN file has been altered and its download cancelled.'

View File

@ -1,11 +1,12 @@
class RPCError(Exception): class RPCError(Exception):
"""Base class for all Remote Procedure Call errors."""
code = None code = None
message = None message = None
class InvalidDCError(RPCError): class InvalidDCError(RPCError):
""" """
The request must be repeated, but directed to a different data center. The request must be repeated, but directed to a different data center.
""" """
code = 303 code = 303
message = 'ERROR_SEE_OTHER' message = 'ERROR_SEE_OTHER'
@ -13,9 +14,9 @@ class InvalidDCError(RPCError):
class BadRequestError(RPCError): class BadRequestError(RPCError):
""" """
The query contains errors. In the event that a request was created The query contains errors. In the event that a request was created
using a form and contains user generated data, the user should be using a form and contains user generated data, the user should be
notified that the data must be corrected before the query is repeated. notified that the data must be corrected before the query is repeated.
""" """
code = 400 code = 400
message = 'BAD_REQUEST' message = 'BAD_REQUEST'
@ -23,8 +24,8 @@ class BadRequestError(RPCError):
class UnauthorizedError(RPCError): class UnauthorizedError(RPCError):
""" """
There was an unauthorized attempt to use functionality available only There was an unauthorized attempt to use functionality available only
to authorized users. to authorized users.
""" """
code = 401 code = 401
message = 'UNAUTHORIZED' message = 'UNAUTHORIZED'
@ -32,8 +33,8 @@ class UnauthorizedError(RPCError):
class ForbiddenError(RPCError): class ForbiddenError(RPCError):
""" """
Privacy violation. For example, an attempt to write a message to Privacy violation. For example, an attempt to write a message to
someone who has blacklisted the current user. someone who has blacklisted the current user.
""" """
code = 403 code = 403
message = 'FORBIDDEN' message = 'FORBIDDEN'
@ -45,7 +46,7 @@ class ForbiddenError(RPCError):
class NotFoundError(RPCError): class NotFoundError(RPCError):
""" """
An attempt to invoke a non-existent object, such as a method. An attempt to invoke a non-existent object, such as a method.
""" """
code = 404 code = 404
message = 'NOT_FOUND' message = 'NOT_FOUND'
@ -57,10 +58,10 @@ class NotFoundError(RPCError):
class FloodError(RPCError): class FloodError(RPCError):
""" """
The maximum allowed number of attempts to invoke the given method The maximum allowed number of attempts to invoke the given method
with the given input parameters has been exceeded. For example, in an with the given input parameters has been exceeded. For example, in an
attempt to request a large number of text messages (SMS) for the same attempt to request a large number of text messages (SMS) for the same
phone number. phone number.
""" """
code = 420 code = 420
message = 'FLOOD' message = 'FLOOD'
@ -68,9 +69,9 @@ class FloodError(RPCError):
class ServerError(RPCError): class ServerError(RPCError):
""" """
An internal server error occurred while a request was being processed An internal server error occurred while a request was being processed
for example, there was a disruption while accessing a database or file for example, there was a disruption while accessing a database or file
storage. storage.
""" """
code = 500 code = 500
message = 'INTERNAL' message = 'INTERNAL'
@ -81,38 +82,42 @@ class ServerError(RPCError):
class BadMessageError(Exception): class BadMessageError(Exception):
"""Occurs when handling a bad_message_notification""" """Occurs when handling a bad_message_notification."""
ErrorMessages = { ErrorMessages = {
16: 16:
'msg_id too low (most likely, client time is wrong it would be worthwhile to ' 'msg_id too low (most likely, client time is wrong it would be '
'synchronize it using msg_id notifications and re-send the original message ' 'worthwhile to synchronize it using msg_id notifications and re-send '
'with the "correct" msg_id or wrap it in a container with a new msg_id if the ' 'the original message with the "correct" msg_id or wrap it in a '
'original message had waited too long on the client to be transmitted).', 'container with a new msg_id if the original message had waited too '
'long on the client to be transmitted).',
17: 17:
'msg_id too high (similar to the previous case, the client time has to be ' 'msg_id too high (similar to the previous case, the client time has '
'synchronized, and the message re-sent with the correct msg_id).', 'to be synchronized, and the message re-sent with the correct msg_id).',
18: 18:
'Incorrect two lower order msg_id bits (the server expects client message msg_id ' 'Incorrect two lower order msg_id bits (the server expects client '
'to be divisible by 4).', 'message msg_id to be divisible by 4).',
19: 19:
'Container msg_id is the same as msg_id of a previously received message ' 'Container msg_id is the same as msg_id of a previously received '
'(this must never happen).', 'message (this must never happen).',
20: 20:
'Message too old, and it cannot be verified whether the server has received a ' 'Message too old, and it cannot be verified whether the server has '
'message with this msg_id or not.', 'received a message with this msg_id or not.',
32: 32:
'msg_seqno too low (the server has already received a message with a lower ' 'msg_seqno too low (the server has already received a message with a '
'msg_id but with either a higher or an equal and odd seqno).', 'lower msg_id but with either a higher or an equal and odd seqno).',
33: 33:
'msg_seqno too high (similarly, there is a message with a higher msg_id but with ' 'msg_seqno too high (similarly, there is a message with a higher '
'either a lower or an equal and odd seqno).', 'msg_id but with either a lower or an equal and odd seqno).',
34: 34:
'An even msg_seqno expected (irrelevant message), but odd received.', 'An even msg_seqno expected (irrelevant message), but odd received.',
35: 'Odd msg_seqno expected (relevant message), but even received.', 35:
'Odd msg_seqno expected (relevant message), but even received.',
48: 48:
'Incorrect server salt (in this case, the bad_server_salt response is received with ' 'Incorrect server salt (in this case, the bad_server_salt response '
'the correct salt, and the message is to be re-sent with it).', 'is received with the correct salt, and the message is to be re-sent '
64: 'Invalid container.' 'with it).',
64:
'Invalid container.'
} }
def __init__(self, code): def __init__(self, code):

View File

@ -1,9 +1,12 @@
"""
This module contains the BinaryReader utility class.
"""
import os import os
from datetime import datetime from datetime import datetime
from io import BufferedReader, BytesIO from io import BufferedReader, BytesIO
from struct import unpack from struct import unpack
from ..errors import InvalidParameterError, TypeNotFoundError from ..errors import TypeNotFoundError
from ..tl.all_tlobjects import tlobjects from ..tl.all_tlobjects import tlobjects
@ -19,8 +22,7 @@ class BinaryReader:
elif stream: elif stream:
self.stream = stream self.stream = stream
else: else:
raise InvalidParameterError( raise ValueError('Either bytes or a stream must be provided')
'Either bytes or a stream must be provided')
self.reader = BufferedReader(self.stream) self.reader = BufferedReader(self.stream)
self._last = None # Should come in handy to spot -404 errors self._last = None # Should come in handy to spot -404 errors
@ -30,32 +32,32 @@ class BinaryReader:
# "All numbers are written as little endian." # "All numbers are written as little endian."
# https://core.telegram.org/mtproto # https://core.telegram.org/mtproto
def read_byte(self): def read_byte(self):
"""Reads a single byte value""" """Reads a single byte value."""
return self.read(1)[0] return self.read(1)[0]
def read_int(self, signed=True): def read_int(self, signed=True):
"""Reads an integer (4 bytes) value""" """Reads an integer (4 bytes) value."""
return int.from_bytes(self.read(4), byteorder='little', signed=signed) return int.from_bytes(self.read(4), byteorder='little', signed=signed)
def read_long(self, signed=True): def read_long(self, signed=True):
"""Reads a long integer (8 bytes) value""" """Reads a long integer (8 bytes) value."""
return int.from_bytes(self.read(8), byteorder='little', signed=signed) return int.from_bytes(self.read(8), byteorder='little', signed=signed)
def read_float(self): def read_float(self):
"""Reads a real floating point (4 bytes) value""" """Reads a real floating point (4 bytes) value."""
return unpack('<f', self.read(4))[0] return unpack('<f', self.read(4))[0]
def read_double(self): def read_double(self):
"""Reads a real floating point (8 bytes) value""" """Reads a real floating point (8 bytes) value."""
return unpack('<d', self.read(8))[0] return unpack('<d', self.read(8))[0]
def read_large_int(self, bits, signed=True): def read_large_int(self, bits, signed=True):
"""Reads a n-bits long integer value""" """Reads a n-bits long integer value."""
return int.from_bytes( return int.from_bytes(
self.read(bits // 8), byteorder='little', signed=signed) self.read(bits // 8), byteorder='little', signed=signed)
def read(self, length): def read(self, length):
"""Read the given amount of bytes""" """Read the given amount of bytes."""
result = self.reader.read(length) result = self.reader.read(length)
if len(result) != length: if len(result) != length:
raise BufferError( raise BufferError(
@ -67,7 +69,7 @@ class BinaryReader:
return result return result
def get_bytes(self): def get_bytes(self):
"""Gets the byte array representing the current buffer as a whole""" """Gets the byte array representing the current buffer as a whole."""
return self.stream.getvalue() return self.stream.getvalue()
# endregion # endregion
@ -75,8 +77,9 @@ class BinaryReader:
# region Telegram custom reading # region Telegram custom reading
def tgread_bytes(self): def tgread_bytes(self):
"""Reads a Telegram-encoded byte array, """
without the need of specifying its length Reads a Telegram-encoded byte array, without the need of
specifying its length.
""" """
first_byte = self.read_byte() first_byte = self.read_byte()
if first_byte == 254: if first_byte == 254:
@ -95,28 +98,28 @@ class BinaryReader:
return data return data
def tgread_string(self): def tgread_string(self):
"""Reads a Telegram-encoded string""" """Reads a Telegram-encoded string."""
return str(self.tgread_bytes(), encoding='utf-8', errors='replace') return str(self.tgread_bytes(), encoding='utf-8', errors='replace')
def tgread_bool(self): def tgread_bool(self):
"""Reads a Telegram boolean value""" """Reads a Telegram boolean value."""
value = self.read_int(signed=False) value = self.read_int(signed=False)
if value == 0x997275b5: # boolTrue if value == 0x997275b5: # boolTrue
return True return True
elif value == 0xbc799737: # boolFalse elif value == 0xbc799737: # boolFalse
return False return False
else: else:
raise ValueError('Invalid boolean code {}'.format(hex(value))) raise RuntimeError('Invalid boolean code {}'.format(hex(value)))
def tgread_date(self): def tgread_date(self):
"""Reads and converts Unix time (used by Telegram) """Reads and converts Unix time (used by Telegram)
into a Python datetime object into a Python datetime object.
""" """
value = self.read_int() value = self.read_int()
return None if value == 0 else datetime.utcfromtimestamp(value) return None if value == 0 else datetime.utcfromtimestamp(value)
def tgread_object(self): def tgread_object(self):
"""Reads a Telegram object""" """Reads a Telegram object."""
constructor_id = self.read_int(signed=False) constructor_id = self.read_int(signed=False)
clazz = tlobjects.get(constructor_id, None) clazz = tlobjects.get(constructor_id, None)
if clazz is None: if clazz is None:
@ -135,9 +138,9 @@ class BinaryReader:
return clazz.from_reader(self) return clazz.from_reader(self)
def tgread_vector(self): def tgread_vector(self):
"""Reads a vector (a list) of Telegram objects""" """Reads a vector (a list) of Telegram objects."""
if 0x1cb5c415 != self.read_int(signed=False): if 0x1cb5c415 != self.read_int(signed=False):
raise ValueError('Invalid constructor code, vector was expected') raise RuntimeError('Invalid constructor code, vector was expected')
count = self.read_int() count = self.read_int()
return [self.tgread_object() for _ in range(count)] return [self.tgread_object() for _ in range(count)]
@ -145,21 +148,23 @@ class BinaryReader:
# endregion # endregion
def close(self): def close(self):
"""Closes the reader, freeing the BytesIO stream."""
self.reader.close() self.reader.close()
# region Position related # region Position related
def tell_position(self): def tell_position(self):
"""Tells the current position on the stream""" """Tells the current position on the stream."""
return self.reader.tell() return self.reader.tell()
def set_position(self, position): def set_position(self, position):
"""Sets the current position on the stream""" """Sets the current position on the stream."""
self.reader.seek(position) self.reader.seek(position)
def seek(self, offset): def seek(self, offset):
"""Seeks the stream position given an offset from the """
current position. The offset may be negative Seeks the stream position given an offset from the current position.
The offset may be negative.
""" """
self.reader.seek(offset, os.SEEK_CUR) self.reader.seek(offset, os.SEEK_CUR)

View File

@ -33,12 +33,13 @@ ENC = 'utf-16le'
def parse(message, delimiters=None, url_re=None): def parse(message, delimiters=None, url_re=None):
""" """
Parses the given message and returns the stripped message and a list Parses the given markdown message and returns its stripped representation
of MessageEntity* using the specified delimiters dictionary (or default plus a list of the MessageEntity's that were found.
if None). The dictionary should be a mapping {delimiter: entity class}.
The url_re(gex) must contain two matching groups: the text to be :param message: the message with markdown-like syntax to be parsed.
clickable and the URL itself, and be utf-16le encoded. :param delimiters: the delimiters to be used, {delimiter: type}.
:param url_re: the URL bytes regex to be used. Must have two groups.
:return: a tuple consisting of (clean message, [message entities]).
""" """
if url_re is None: if url_re is None:
url_re = DEFAULT_URL_RE url_re = DEFAULT_URL_RE
@ -183,8 +184,13 @@ def unparse(text, entities, delimiters=None, url_fmt=None):
def get_inner_text(text, entity): def get_inner_text(text, entity):
"""Gets the inner text that's surrounded by the given entity or entities. """
For instance: text = 'hey!', entity = MessageEntityBold(2, 2) -> 'y!'. Gets the inner text that's surrounded by the given entity or entities.
For instance: text = 'hey!', entity = MessageEntityBold(2, 2) -> 'y!'.
:param text: the original text.
:param entity: the entity or entities that must be matched.
:return: a single result or a list of the text surrounded by the entities.
""" """
if isinstance(entity, TLObject): if isinstance(entity, TLObject):
entity = (entity,) entity = (entity,)

View File

@ -1,3 +1,6 @@
"""
This module holds a rough implementation of the C# TCP client.
"""
# Python rough implementation of a C# TCP client # Python rough implementation of a C# TCP client
import asyncio import asyncio
import errno import errno
@ -13,7 +16,14 @@ CONN_RESET_ERRNOS = {
class TcpClient: class TcpClient:
"""A simple TCP client to ease the work with sockets and proxies."""
def __init__(self, proxy=None, timeout=timedelta(seconds=5), loop=None): def __init__(self, proxy=None, timeout=timedelta(seconds=5), loop=None):
"""
Initializes the TCP client.
:param proxy: the proxy to be used, if any.
:param timeout: the timeout for connect, read and write operations.
"""
self.proxy = proxy self.proxy = proxy
self._socket = None self._socket = None
self._loop = loop if loop else asyncio.get_event_loop() self._loop = loop if loop else asyncio.get_event_loop()
@ -23,7 +33,7 @@ class TcpClient:
elif isinstance(timeout, (int, float)): elif isinstance(timeout, (int, float)):
self.timeout = float(timeout) self.timeout = float(timeout)
else: else:
raise ValueError('Invalid timeout type', type(timeout)) raise TypeError('Invalid timeout type: {}'.format(type(timeout)))
def _recreate_socket(self, mode): def _recreate_socket(self, mode):
if self.proxy is None: if self.proxy is None:
@ -39,8 +49,11 @@ class TcpClient:
self._socket.setblocking(False) self._socket.setblocking(False)
async def connect(self, ip, port): async def connect(self, ip, port):
"""Connects to the specified IP and port number. """
'timeout' must be given in seconds Tries connecting forever to IP:port unless an OSError is raised.
:param ip: the IP to connect to.
:param port: the port to connect to.
""" """
if ':' in ip: # IPv6 if ':' in ip: # IPv6
# The address needs to be surrounded by [] as discussed on PR#425 # The address needs to be surrounded by [] as discussed on PR#425
@ -78,12 +91,13 @@ class TcpClient:
raise raise
def _get_connected(self): def _get_connected(self):
"""Determines whether the client is connected or not."""
return self._socket is not None and self._socket.fileno() >= 0 return self._socket is not None and self._socket.fileno() >= 0
connected = property(fget=_get_connected) connected = property(fget=_get_connected)
def close(self): def close(self):
"""Closes the connection""" """Closes the connection."""
try: try:
if self._socket is not None: if self._socket is not None:
self._socket.shutdown(socket.SHUT_RDWR) self._socket.shutdown(socket.SHUT_RDWR)
@ -94,7 +108,11 @@ class TcpClient:
self._socket = None self._socket = None
async def write(self, data): async def write(self, data):
"""Writes (sends) the specified bytes to the connected peer""" """
Writes (sends) the specified bytes to the connected peer.
:param data: the data to send.
"""
if self._socket is None: if self._socket is None:
self._raise_connection_reset() self._raise_connection_reset()
@ -106,7 +124,7 @@ class TcpClient:
) )
except asyncio.TimeoutError as e: except asyncio.TimeoutError as e:
raise TimeoutError() from e raise TimeoutError() from e
except BrokenPipeError: except ConnectionError:
self._raise_connection_reset() self._raise_connection_reset()
except OSError as e: except OSError as e:
if e.errno in CONN_RESET_ERRNOS: if e.errno in CONN_RESET_ERRNOS:
@ -115,8 +133,11 @@ class TcpClient:
raise raise
async def read(self, size): async def read(self, size):
"""Reads (receives) a whole block of 'size bytes """
from the connected peer. Reads (receives) a whole block of size bytes from the connected peer.
:param size: the size of the block to be read.
:return: the read data with len(data) == size.
""" """
with BufferedWriter(BytesIO(), buffer_size=size) as buffer: with BufferedWriter(BytesIO(), buffer_size=size) as buffer:
@ -132,6 +153,8 @@ class TcpClient:
) )
except asyncio.TimeoutError as e: except asyncio.TimeoutError as e:
raise TimeoutError() from e raise TimeoutError() from e
except ConnectionError:
self._raise_connection_reset()
except OSError as e: except OSError as e:
if e.errno in CONN_RESET_ERRNOS: if e.errno in CONN_RESET_ERRNOS:
self._raise_connection_reset() self._raise_connection_reset()
@ -149,6 +172,7 @@ class TcpClient:
return buffer.raw.getvalue() return buffer.raw.getvalue()
def _raise_connection_reset(self): def _raise_connection_reset(self):
"""Disconnects the client and raises ConnectionResetError."""
self.close() # Connection reset -> flag as socket closed self.close() # Connection reset -> flag as socket closed
raise ConnectionResetError('The server has closed the connection.') raise ConnectionResetError('The server has closed the connection.')

View File

@ -1,3 +1,7 @@
"""
This module contains several classes regarding network, low level connection
with Telegram's servers and the protocol used (TCP full, abridged, etc.).
"""
from .mtproto_plain_sender import MtProtoPlainSender from .mtproto_plain_sender import MtProtoPlainSender
from .authenticator import do_authentication from .authenticator import do_authentication
from .mtproto_sender import MtProtoSender from .mtproto_sender import MtProtoSender

View File

@ -1,3 +1,7 @@
"""
This module contains several functions that authenticate the client machine
with Telegram's servers, effectively creating an authorization key.
"""
import os import os
import time import time
from hashlib import sha1 from hashlib import sha1
@ -18,6 +22,14 @@ from ..tl.functions import (
async def do_authentication(connection, retries=5): async def do_authentication(connection, retries=5):
"""
Performs the authentication steps on the given connection.
Raises an error if all attempts fail.
:param connection: the connection to be used (must be connected).
:param retries: how many times should we retry on failure.
:return:
"""
if not retries or retries < 0: if not retries or retries < 0:
retries = 1 retries = 1
@ -32,9 +44,11 @@ async def do_authentication(connection, retries=5):
async def _do_authentication(connection): async def _do_authentication(connection):
"""Executes the authentication process with the Telegram servers. """
If no error is raised, returns both the authorization key and the Executes the authentication process with the Telegram servers.
time offset.
:param connection: the connection to be used (must be connected).
:return: returns a (authorization key, time offset) tuple.
""" """
sender = MtProtoPlainSender(connection) sender = MtProtoPlainSender(connection)
@ -195,8 +209,12 @@ async def _do_authentication(connection):
def get_int(byte_array, signed=True): def get_int(byte_array, signed=True):
"""Gets the specified integer from its byte array. """
This should be used by the authenticator, Gets the specified integer from its byte array.
who requires the data to be in big endian This should be used by this module alone, as it works with big endian.
:param byte_array: the byte array representing th integer.
:param signed: whether the number is signed or not.
:return: the integer representing the given byte array.
""" """
return int.from_bytes(byte_array, byteorder='big', signed=signed) return int.from_bytes(byte_array, byteorder='big', signed=signed)

View File

@ -1,3 +1,7 @@
"""
This module holds both the Connection class and the ConnectionMode enum,
which specifies the protocol to be used by the Connection.
"""
import errno import errno
import os import os
import struct import struct
@ -34,16 +38,24 @@ class ConnectionMode(Enum):
class Connection: class Connection:
"""Represents an abstract connection (TCP, TCP abridged...). """
'mode' must be any of the ConnectionMode enumeration. Represents an abstract connection (TCP, TCP abridged...).
'mode' must be any of the ConnectionMode enumeration.
Note that '.send()' and '.recv()' refer to messages, which Note that '.send()' and '.recv()' refer to messages, which
will be packed accordingly, whereas '.write()' and '.read()' will be packed accordingly, whereas '.write()' and '.read()'
work on plain bytes, with no further additions. work on plain bytes, with no further additions.
""" """
def __init__(self, mode=ConnectionMode.TCP_FULL, def __init__(self, mode=ConnectionMode.TCP_FULL,
proxy=None, timeout=timedelta(seconds=5), loop=None): proxy=None, timeout=timedelta(seconds=5), loop=None):
"""
Initializes a new connection.
:param mode: the ConnectionMode to be used.
:param proxy: whether to use a proxy or not.
:param timeout: timeout to be used for all operations.
"""
self._mode = mode self._mode = mode
self._send_counter = 0 self._send_counter = 0
self._aes_encrypt, self._aes_decrypt = None, None self._aes_encrypt, self._aes_decrypt = None, None
@ -74,6 +86,12 @@ class Connection:
setattr(self, 'read', self._read_plain) setattr(self, 'read', self._read_plain)
async def connect(self, ip, port): async def connect(self, ip, port):
"""
Estabilishes a connection to IP:port.
:param ip: the IP to connect to.
:param port: the port to connect to.
"""
try: try:
await self.conn.connect(ip, port) await self.conn.connect(ip, port)
except OSError as e: except OSError as e:
@ -91,9 +109,13 @@ class Connection:
await self._setup_obfuscation() await self._setup_obfuscation()
def get_timeout(self): def get_timeout(self):
"""Returns the timeout used by the connection."""
return self.conn.timeout return self.conn.timeout
async def _setup_obfuscation(self): async def _setup_obfuscation(self):
"""
Sets up the obfuscated protocol.
"""
# Obfuscated messages secrets cannot start with any of these # Obfuscated messages secrets cannot start with any of these
keywords = (b'PVrG', b'GET ', b'POST', b'\xee' * 4) keywords = (b'PVrG', b'GET ', b'POST', b'\xee' * 4)
while True: while True:
@ -121,13 +143,19 @@ class Connection:
await self.conn.write(bytes(random)) await self.conn.write(bytes(random))
def is_connected(self): def is_connected(self):
"""
Determines whether the connection is alive or not.
:return: true if it's connected.
"""
return self.conn.connected return self.conn.connected
def close(self): def close(self):
"""Closes the connection."""
self.conn.close() self.conn.close()
def clone(self): def clone(self):
"""Creates a copy of this Connection""" """Creates a copy of this Connection."""
return Connection( return Connection(
mode=self._mode, proxy=self.conn.proxy, timeout=self.conn.timeout mode=self._mode, proxy=self.conn.proxy, timeout=self.conn.timeout
) )
@ -140,6 +168,15 @@ class Connection:
raise ValueError('Invalid connection mode specified: ' + str(self._mode)) raise ValueError('Invalid connection mode specified: ' + str(self._mode))
async def _recv_tcp_full(self): async def _recv_tcp_full(self):
"""
Receives a message from the network,
internally encoded using the TCP full protocol.
May raise InvalidChecksumError if the received data doesn't
match its valid checksum.
:return: the read message payload.
"""
# TODO We don't want another call to this method that could # TODO We don't want another call to this method that could
# potentially await on another self.read(n). Is this guaranteed # potentially await on another self.read(n). Is this guaranteed
# by asyncio? # by asyncio?
@ -156,9 +193,21 @@ class Connection:
return body return body
async def _recv_intermediate(self): async def _recv_intermediate(self):
"""
Receives a message from the network,
internally encoded using the TCP intermediate protocol.
:return: the read message payload.
"""
return await self.read(struct.unpack('<i', await self.read(4))[0]) return await self.read(struct.unpack('<i', await self.read(4))[0])
async def _recv_abridged(self): async def _recv_abridged(self):
"""
Receives a message from the network,
internally encoded using the TCP abridged protocol.
:return: the read message payload.
"""
length = struct.unpack('<B', await self.read(1))[0] length = struct.unpack('<B', await self.read(1))[0]
if length >= 127: if length >= 127:
length = struct.unpack('<i', await self.read(3) + b'\0')[0] length = struct.unpack('<i', await self.read(3) + b'\0')[0]
@ -175,6 +224,12 @@ class Connection:
raise ValueError('Invalid connection mode specified: ' + str(self._mode)) raise ValueError('Invalid connection mode specified: ' + str(self._mode))
async def _send_tcp_full(self, message): async def _send_tcp_full(self, message):
"""
Encapsulates and sends the given message payload
using the TCP full mode (length, sequence, message, crc32).
:param message: the message to be sent.
"""
# https://core.telegram.org/mtproto#tcp-transport # https://core.telegram.org/mtproto#tcp-transport
# total length, sequence number, packet and checksum (CRC32) # total length, sequence number, packet and checksum (CRC32)
length = len(message) + 12 length = len(message) + 12
@ -184,9 +239,21 @@ class Connection:
await self.write(data + crc) await self.write(data + crc)
async def _send_intermediate(self, message): async def _send_intermediate(self, message):
"""
Encapsulates and sends the given message payload
using the TCP intermediate mode (length, message).
:param message: the message to be sent.
"""
await self.write(struct.pack('<i', len(message)) + message) await self.write(struct.pack('<i', len(message)) + message)
async def _send_abridged(self, message): async def _send_abridged(self, message):
"""
Encapsulates and sends the given message payload
using the TCP abridged mode (short length, message).
:param message: the message to be sent.
"""
length = len(message) >> 2 length = len(message) >> 2
if length < 127: if length < 127:
length = struct.pack('B', length) length = struct.pack('B', length)
@ -203,9 +270,21 @@ class Connection:
raise ValueError('Invalid connection mode specified: ' + str(self._mode)) raise ValueError('Invalid connection mode specified: ' + str(self._mode))
async def _read_plain(self, length): async def _read_plain(self, length):
"""
Reads data from the socket connection.
:param length: how many bytes should be read.
:return: a byte sequence with len(data) == length
"""
return await self.conn.read(length) return await self.conn.read(length)
async def _read_obfuscated(self, length): async def _read_obfuscated(self, length):
"""
Reads data and decrypts from the socket connection.
:param length: how many bytes should be read.
:return: the decrypted byte sequence with len(data) == length
"""
return self._aes_decrypt.encrypt(await self.conn.read(length)) return self._aes_decrypt.encrypt(await self.conn.read(length))
# endregion # endregion
@ -216,9 +295,20 @@ class Connection:
raise ValueError('Invalid connection mode specified: ' + str(self._mode)) raise ValueError('Invalid connection mode specified: ' + str(self._mode))
async def _write_plain(self, data): async def _write_plain(self, data):
"""
Writes the given data through the socket connection.
:param data: the data in bytes to be written.
"""
await self.conn.write(data) await self.conn.write(data)
async def _write_obfuscated(self, data): async def _write_obfuscated(self, data):
"""
Writes the given data through the socket connection,
using the obfuscated mode (AES encryption is applied on top).
:param data: the data in bytes to be written.
"""
await self.conn.write(self._aes_encrypt.encrypt(data)) await self.conn.write(self._aes_encrypt.encrypt(data))
# endregion # endregion

View File

@ -1,3 +1,7 @@
"""
This module contains the class used to communicate with Telegram's servers
in plain text, when no authorization key has been created yet.
"""
import struct import struct
import time import time
@ -6,32 +10,47 @@ from ..extensions import BinaryReader
class MtProtoPlainSender: class MtProtoPlainSender:
"""MTProto Mobile Protocol plain sender """
(https://core.telegram.org/mtproto/description#unencrypted-messages) MTProto Mobile Protocol plain sender
(https://core.telegram.org/mtproto/description#unencrypted-messages)
""" """
def __init__(self, connection): def __init__(self, connection):
"""
Initializes the MTProto plain sender.
:param connection: the Connection to be used.
"""
self._sequence = 0 self._sequence = 0
self._time_offset = 0 self._time_offset = 0
self._last_msg_id = 0 self._last_msg_id = 0
self._connection = connection self._connection = connection
async def connect(self): async def connect(self):
"""Connects to Telegram's servers."""
await self._connection.connect() await self._connection.connect()
def disconnect(self): def disconnect(self):
"""Disconnects from Telegram's servers."""
self._connection.close() self._connection.close()
async def send(self, data): async def send(self, data):
"""Sends a plain packet (auth_key_id = 0) containing the """
given message body (data) Sends a plain packet (auth_key_id = 0) containing the
given message body (data).
:param data: the data to be sent.
""" """
await self._connection.send( await self._connection.send(
struct.pack('<QQi', 0, self._get_new_msg_id(), len(data)) + data struct.pack('<QQi', 0, self._get_new_msg_id(), len(data)) + data
) )
async def receive(self): async def receive(self):
"""Receives a plain packet, returning the body of the response""" """
Receives a plain packet from the network.
:return: the response body.
"""
body = await self._connection.recv() body = await self._connection.recv()
if body == b'l\xfe\xff\xff': # -404 little endian signed if body == b'l\xfe\xff\xff': # -404 little endian signed
# Broken authorization, must reset the auth key # Broken authorization, must reset the auth key
@ -46,7 +65,7 @@ class MtProtoPlainSender:
return response return response
def _get_new_msg_id(self): def _get_new_msg_id(self):
"""Generates a new message ID based on the current time since epoch""" """Generates a new message ID based on the current time since epoch."""
# See core.telegram.org/mtproto/description#message-identifier-msg-id # See core.telegram.org/mtproto/description#message-identifier-msg-id
now = time.time() now = time.time()
nanoseconds = int((now - int(now)) * 1e+9) nanoseconds = int((now - int(now)) * 1e+9)

View File

@ -1,3 +1,7 @@
"""
This module contains the class used to communicate with Telegram's servers
encrypting every packet, and relies on a valid AuthKey in the used Session.
"""
import gzip import gzip
import logging import logging
import struct import struct
@ -19,7 +23,7 @@ from ..tl.types import (
) )
from ..tl.functions.auth import LogOutRequest from ..tl.functions.auth import LogOutRequest
logging.getLogger(__name__).addHandler(logging.NullHandler()) __log__ = logging.getLogger(__name__)
class MtProtoSender: class MtProtoSender:
@ -33,38 +37,50 @@ class MtProtoSender:
""" """
def __init__(self, session, connection, loop=None): def __init__(self, session, connection, loop=None):
"""Creates a new MtProtoSender configured to send messages through """
'connection' and using the parameters from 'session'. Initializes a new MTProto sender.
:param session:
the Session to be used with this sender. Must contain the IP and
port of the server, salt, ID, and AuthKey,
:param connection:
the Connection to be used.
:param loop:
the asyncio loop to be used, or the default one.
""" """
self.session = session self.session = session
self.connection = connection self.connection = connection
self._loop = loop if loop else asyncio.get_event_loop() self._loop = loop if loop else asyncio.get_event_loop()
self._logger = logging.getLogger(__name__)
# Requests (as msg_id: Message) sent waiting to be received # Requests (as msg_id: Message) sent waiting to be received
self._pending_receive = {} self._pending_receive = {}
async def connect(self): async def connect(self):
"""Connects to the server""" """Connects to the server."""
await self.connection.connect(self.session.server_address, self.session.port) await self.connection.connect(self.session.server_address, self.session.port)
def is_connected(self): def is_connected(self):
"""
Determines whether the sender is connected or not.
:return: true if the sender is connected.
"""
return self.connection.is_connected() return self.connection.is_connected()
def disconnect(self): def disconnect(self):
"""Disconnects from the server""" """Disconnects from the server."""
self.connection.close() self.connection.close()
self._clear_all_pending() self._clear_all_pending()
def clone(self):
"""Creates a copy of this MtProtoSender as a new connection"""
return MtProtoSender(self.session, self.connection.clone(), self._loop)
# region Send and receive # region Send and receive
async def send(self, *requests): async def send(self, *requests):
"""Sends the specified MTProtoRequest, previously sending any message """
which needed confirmation.""" Sends the specified TLObject(s) (which must be requests),
and acknowledging any message which needed confirmation.
:param requests: the requests to be sent.
"""
# Prepare the event of every request # Prepare the event of every request
for r in requests: for r in requests:
@ -90,18 +106,23 @@ class MtProtoSender:
await self._send_message(message) await self._send_message(message)
async def _send_acknowledge(self, msg_id): async def _send_acknowledge(self, msg_id):
"""Sends a message acknowledge for the given msg_id""" """Sends a message acknowledge for the given msg_id."""
await self._send_message(TLMessage(self.session, MsgsAck([msg_id]))) await self._send_message(TLMessage(self.session, MsgsAck([msg_id])))
async def receive(self, update_state): async def receive(self, update_state):
"""Receives a single message from the connected endpoint. """
Receives a single message from the connected endpoint.
This method returns nothing, and will only affect other parts This method returns nothing, and will only affect other parts
of the MtProtoSender such as the updates callback being fired of the MtProtoSender such as the updates callback being fired
or a pending request being confirmed. or a pending request being confirmed.
Any unhandled object (likely updates) will be passed to Any unhandled object (likely updates) will be passed to
update_state.process(TLObject). update_state.process(TLObject).
:param update_state:
the UpdateState that will process all the received
Update and Updates objects.
""" """
try: try:
body = await self.connection.recv() body = await self.connection.recv()
@ -113,6 +134,9 @@ class MtProtoSender:
# "This packet should be skipped"; since this may have # "This packet should be skipped"; since this may have
# been a result for a request, invalidate every request # been a result for a request, invalidate every request
# and just re-invoke them to avoid problems # and just re-invoke them to avoid problems
__log__.exception('Error while receiving server response. '
'%d pending request(s) will be ignored',
len(self._pending_receive))
self._clear_all_pending() self._clear_all_pending()
return return
@ -126,10 +150,13 @@ class MtProtoSender:
# region Low level processing # region Low level processing
async def _send_message(self, message): async def _send_message(self, message):
"""Sends the given Message(TLObject) encrypted through the network""" """
Sends the given encrypted through the network.
:param message: the TLMessage to be sent.
"""
plain_text = \ plain_text = \
struct.pack('<QQ', self.session.salt, self.session.id) \ struct.pack('<qq', self.session.salt, self.session.id) \
+ bytes(message) + bytes(message)
msg_key = utils.calc_msg_key(plain_text) msg_key = utils.calc_msg_key(plain_text)
@ -141,7 +168,12 @@ class MtProtoSender:
await self.connection.send(result) await self.connection.send(result)
def _decode_msg(self, body): def _decode_msg(self, body):
"""Decodes an received encrypted message body bytes""" """
Decodes the body of the payload received from the network.
:param body: the body to be decoded.
:return: a tuple of (decoded message, remote message id, remote seq).
"""
message = None message = None
remote_msg_id = None remote_msg_id = None
remote_sequence = None remote_sequence = None
@ -172,100 +204,113 @@ class MtProtoSender:
return message, remote_msg_id, remote_sequence return message, remote_msg_id, remote_sequence
async def _process_msg(self, msg_id, sequence, reader, state): async def _process_msg(self, msg_id, sequence, reader, state):
"""Processes and handles a Telegram message.
Returns True if the message was handled correctly and doesn't
need to be skipped. Returns False otherwise.
""" """
Processes the message read from the network inside reader.
:param msg_id: the ID of the message.
:param sequence: the sequence of the message.
:param reader: the BinaryReader that contains the message.
:param state: the current UpdateState.
:return: true if the message was handled correctly, false otherwise.
"""
# TODO Check salt, session_id and sequence_number # TODO Check salt, session_id and sequence_number
code = reader.read_int(signed=False) code = reader.read_int(signed=False)
reader.seek(-4) reader.seek(-4)
# The following codes are "parsed manually" # These are a bit of special case, not yet generated by the code gen
if code == 0xf35c6d01: # rpc_result, (response of an RPC call) if code == 0xf35c6d01: # rpc_result, (response of an RPC call)
__log__.debug('Processing Remote Procedure Call result')
return await self._handle_rpc_result(msg_id, sequence, reader) return await self._handle_rpc_result(msg_id, sequence, reader)
if code == Pong.CONSTRUCTOR_ID:
return await self._handle_pong(msg_id, sequence, reader)
if code == MessageContainer.CONSTRUCTOR_ID: if code == MessageContainer.CONSTRUCTOR_ID:
__log__.debug('Processing container result')
return await self._handle_container(msg_id, sequence, reader, state) return await self._handle_container(msg_id, sequence, reader, state)
if code == GzipPacked.CONSTRUCTOR_ID: if code == GzipPacked.CONSTRUCTOR_ID:
__log__.debug('Processing gzipped result')
return await self._handle_gzip_packed(msg_id, sequence, reader, state) return await self._handle_gzip_packed(msg_id, sequence, reader, state)
if code == BadServerSalt.CONSTRUCTOR_ID: if code not in tlobjects:
return await self._handle_bad_server_salt(msg_id, sequence, reader) __log__.warning(
'Unknown message with ID %d, data left in the buffer %s',
hex(code), repr(reader.get_bytes()[reader.tell_position():])
)
return False
if code == BadMsgNotification.CONSTRUCTOR_ID: obj = reader.tgread_object()
return await self._handle_bad_msg_notification(msg_id, sequence, reader) __log__.debug('Processing %s result', type(obj).__name__)
if code == MsgDetailedInfo.CONSTRUCTOR_ID: if isinstance(obj, Pong):
return await self._handle_msg_detailed_info(msg_id, sequence, reader) return await self._handle_pong(msg_id, sequence, obj)
if code == MsgNewDetailedInfo.CONSTRUCTOR_ID: if isinstance(obj, BadServerSalt):
return await self._handle_msg_new_detailed_info(msg_id, sequence, reader) return await self._handle_bad_server_salt(msg_id, sequence, obj)
if code == NewSessionCreated.CONSTRUCTOR_ID: if isinstance(obj, BadMsgNotification):
return await self._handle_new_session_created(msg_id, sequence, reader) return await self._handle_bad_msg_notification(msg_id, sequence, obj)
if code == MsgsAck.CONSTRUCTOR_ID: # may handle the request we wanted if isinstance(obj, MsgDetailedInfo):
ack = reader.tgread_object() return await self._handle_msg_detailed_info(msg_id, sequence, obj)
assert isinstance(ack, MsgsAck)
# Ignore every ack request *unless* when logging out, if isinstance(obj, MsgNewDetailedInfo):
return await self._handle_msg_new_detailed_info(msg_id, sequence, obj)
if isinstance(obj, NewSessionCreated):
return await self._handle_new_session_created(msg_id, sequence, obj)
if isinstance(obj, MsgsAck): # may handle the request we wanted
# Ignore every ack request *unless* when logging out, when it's
# when it seems to only make sense. We also need to set a non-None # when it seems to only make sense. We also need to set a non-None
# result since Telegram doesn't send the response for these. # result since Telegram doesn't send the response for these.
for msg_id in ack.msg_ids: for msg_id in obj.msg_ids:
r = self._pop_request_of_type(msg_id, LogOutRequest) r = self._pop_request_of_type(msg_id, LogOutRequest)
if r: if r:
r.result = True # Telegram won't send this value r.result = True # Telegram won't send this value
r.confirm_received.set() r.confirm_received.set()
self._logger.debug('Message ack confirmed', r)
return True return True
# If the code is not parsed manually then it should be a TLObject. # If the object isn't any of the above, then it should be an Update.
if code in tlobjects: self.session.process_entities(obj)
result = reader.tgread_object() if state:
self.session.process_entities(result) state.process(obj)
if state:
state.process(result)
return True return True
self._logger.debug(
'[WARN] Unknown message: {}, data left in the buffer: {}'
.format(
hex(code), repr(reader.get_bytes()[reader.tell_position():])
)
)
return False
# endregion # endregion
# region Message handling # region Message handling
def _pop_request(self, msg_id): def _pop_request(self, msg_id):
"""Pops a pending REQUEST from self._pending_receive, or """
returns None if it's not found. Pops a pending **request** from self._pending_receive.
:param msg_id: the ID of the message that belongs to the request.
:return: the request, or None if it wasn't found.
""" """
message = self._pending_receive.pop(msg_id, None) message = self._pending_receive.pop(msg_id, None)
if message: if message:
return message.request return message.request
def _pop_request_of_type(self, msg_id, t): def _pop_request_of_type(self, msg_id, t):
"""Pops a pending REQUEST from self._pending_receive if it matches """
the given type, or returns None if it's not found/doesn't match. Pops a pending **request** from self._pending_receive.
:param msg_id: the ID of the message that belongs to the request.
:param t: the type of the desired request.
:return: the request matching the type t, or None if it wasn't found.
""" """
message = self._pending_receive.get(msg_id, None) message = self._pending_receive.get(msg_id, None)
if message and isinstance(message.request, t): if message and isinstance(message.request, t):
return self._pending_receive.pop(msg_id).request return self._pending_receive.pop(msg_id).request
def _pop_requests_of_container(self, container_msg_id): def _pop_requests_of_container(self, container_msg_id):
"""Pops the pending requests (plural) from self._pending_receive if """
they were sent on a container that matches container_msg_id. Pops pending **requests** from self._pending_receive.
:param container_msg_id: the ID of the container.
:return: the requests that belong to the given container. May be empty.
""" """
msgs = [msg for msg in self._pending_receive.values() msgs = [msg for msg in self._pending_receive.values()
if msg.container_msg_id == container_msg_id] if msg.container_msg_id == container_msg_id]
@ -276,13 +321,19 @@ class MtProtoSender:
return requests return requests
def _clear_all_pending(self): def _clear_all_pending(self):
"""
Clears all pending requests, and flags them all as received.
"""
for r in self._pending_receive.values(): for r in self._pending_receive.values():
r.request.confirm_received.set() r.request.confirm_received.set()
self._pending_receive.clear() self._pending_receive.clear()
async def _resend_request(self, msg_id): async def _resend_request(self, msg_id):
"""Re-sends the request that belongs to a certain msg_id. This may """
also be the msg_id of a container if they were sent in one. Re-sends the request that belongs to a certain msg_id. This may
also be the msg_id of a container if they were sent in one.
:param msg_id: the ID of the request to be resent.
""" """
request = self._pop_request(msg_id) request = self._pop_request(msg_id)
if request: if request:
@ -294,21 +345,31 @@ class MtProtoSender:
self._logger.debug('Resending container of requests') self._logger.debug('Resending container of requests')
await self.send(*requests) await self.send(*requests)
async def _handle_pong(self, msg_id, sequence, reader): def _handle_pong(self, msg_id, sequence, pong):
self._logger.debug('Handling pong') """
pong = reader.tgread_object() Handles a Pong response.
assert isinstance(pong, Pong)
:param msg_id: the ID of the message.
:param sequence: the sequence of the message.
:param reader: the reader containing the Pong.
:return: true, as it always succeeds.
"""
request = self._pop_request(pong.msg_id) request = self._pop_request(pong.msg_id)
if request: if request:
self._logger.debug('Pong confirmed a request')
request.result = pong request.result = pong
request.confirm_received.set() request.confirm_received.set()
return True return True
async def _handle_container(self, msg_id, sequence, reader, state): async def _handle_container(self, msg_id, sequence, reader, state):
self._logger.debug('Handling container') """
Handles a MessageContainer response.
:param msg_id: the ID of the message.
:param sequence: the sequence of the message.
:param reader: the reader containing the MessageContainer.
:return: true, as it always succeeds.
"""
for inner_msg_id, _, inner_len in MessageContainer.iter_read(reader): for inner_msg_id, _, inner_len in MessageContainer.iter_read(reader):
begin_position = reader.tell_position() begin_position = reader.tell_position()
@ -324,15 +385,16 @@ class MtProtoSender:
return True return True
async def _handle_bad_server_salt(self, msg_id, sequence, reader): async def _handle_bad_server_salt(self, msg_id, sequence, bad_salt):
self._logger.debug('Handling bad server salt') """
bad_salt = reader.tgread_object() Handles a BadServerSalt response.
assert isinstance(bad_salt, BadServerSalt)
# Our salt is unsigned, but the objects work with signed salts :param msg_id: the ID of the message.
self.session.salt = struct.unpack( :param sequence: the sequence of the message.
'<Q', struct.pack('<q', bad_salt.new_server_salt) :param reader: the reader containing the BadServerSalt.
)[0] :return: true, as it always succeeds.
"""
self.session.salt = bad_salt.new_server_salt
self.session.save() self.session.save()
# "the bad_server_salt response is received with the # "the bad_server_salt response is received with the
@ -341,60 +403,91 @@ class MtProtoSender:
return True return True
async def _handle_bad_msg_notification(self, msg_id, sequence, reader): async def _handle_bad_msg_notification(self, msg_id, sequence, bad_msg):
self._logger.debug('Handling bad message notification') """
bad_msg = reader.tgread_object() Handles a BadMessageError response.
assert isinstance(bad_msg, BadMsgNotification)
:param msg_id: the ID of the message.
:param sequence: the sequence of the message.
:param reader: the reader containing the BadMessageError.
:return: true, as it always succeeds.
"""
error = BadMessageError(bad_msg.error_code) error = BadMessageError(bad_msg.error_code)
__log__.warning('Read bad msg notification %s: %s', bad_msg, error)
if bad_msg.error_code in (16, 17): if bad_msg.error_code in (16, 17):
# sent msg_id too low or too high (respectively). # sent msg_id too low or too high (respectively).
# Use the current msg_id to determine the right time offset. # Use the current msg_id to determine the right time offset.
self.session.update_time_offset(correct_msg_id=msg_id) self.session.update_time_offset(correct_msg_id=msg_id)
self._logger.debug('Read Bad Message error: ' + str(error)) __log__.info('Attempting to use the correct time offset')
self._logger.debug('Attempting to use the correct time offset.')
await self._resend_request(bad_msg.bad_msg_id) await self._resend_request(bad_msg.bad_msg_id)
return True return True
elif bad_msg.error_code == 32: elif bad_msg.error_code == 32:
# msg_seqno too low, so just pump it up by some "large" amount # msg_seqno too low, so just pump it up by some "large" amount
# TODO A better fix would be to start with a new fresh session ID # TODO A better fix would be to start with a new fresh session ID
self.session._sequence += 64 self.session._sequence += 64
__log__.info('Attempting to set the right higher sequence')
await self._resend_request(bad_msg.bad_msg_id) await self._resend_request(bad_msg.bad_msg_id)
return True return True
elif bad_msg.error_code == 33: elif bad_msg.error_code == 33:
# msg_seqno too high never seems to happen but just in case # msg_seqno too high never seems to happen but just in case
self.session._sequence -= 16 self.session._sequence -= 16
__log__.info('Attempting to set the right lower sequence')
await self._resend_request(bad_msg.bad_msg_id) await self._resend_request(bad_msg.bad_msg_id)
return True return True
else: else:
raise error raise error
async def _handle_msg_detailed_info(self, msg_id, sequence, reader): async def _handle_msg_detailed_info(self, msg_id, sequence, msg_new):
msg_new = reader.tgread_object() """
assert isinstance(msg_new, MsgDetailedInfo) Handles a MsgDetailedInfo response.
:param msg_id: the ID of the message.
:param sequence: the sequence of the message.
:param reader: the reader containing the MsgDetailedInfo.
:return: true, as it always succeeds.
"""
# TODO For now, simply ack msg_new.answer_msg_id # TODO For now, simply ack msg_new.answer_msg_id
# Relevant tdesktop source code: https://goo.gl/VvpCC6 # Relevant tdesktop source code: https://goo.gl/VvpCC6
await self._send_acknowledge(msg_new.answer_msg_id) await self._send_acknowledge(msg_new.answer_msg_id)
return True return True
async def _handle_msg_new_detailed_info(self, msg_id, sequence, reader): async def _handle_msg_new_detailed_info(self, msg_id, sequence, msg_new):
msg_new = reader.tgread_object() """
assert isinstance(msg_new, MsgNewDetailedInfo) Handles a MsgNewDetailedInfo response.
:param msg_id: the ID of the message.
:param sequence: the sequence of the message.
:param reader: the reader containing the MsgNewDetailedInfo.
:return: true, as it always succeeds.
"""
# TODO For now, simply ack msg_new.answer_msg_id # TODO For now, simply ack msg_new.answer_msg_id
# Relevant tdesktop source code: https://goo.gl/G7DPsR # Relevant tdesktop source code: https://goo.gl/G7DPsR
await self._send_acknowledge(msg_new.answer_msg_id) await self._send_acknowledge(msg_new.answer_msg_id)
return True return True
async def _handle_new_session_created(self, msg_id, sequence, reader): async def _handle_new_session_created(self, msg_id, sequence, new_session):
new_session = reader.tgread_object() """
assert isinstance(new_session, NewSessionCreated) Handles a NewSessionCreated response.
:param msg_id: the ID of the message.
:param sequence: the sequence of the message.
:param reader: the reader containing the NewSessionCreated.
:return: true, as it always succeeds.
"""
self.session.salt = new_session.server_salt
# TODO https://goo.gl/LMyN7A # TODO https://goo.gl/LMyN7A
return True return True
async def _handle_rpc_result(self, msg_id, sequence, reader): async def _handle_rpc_result(self, msg_id, sequence, reader):
self._logger.debug('Handling RPC result') """
Handles a RPCResult response.
:param msg_id: the ID of the message.
:param sequence: the sequence of the message.
:param reader: the reader containing the RPCResult.
:return: true if the request ID to which this result belongs is found,
false otherwise (meaning nothing was read).
"""
reader.read_int(signed=False) # code reader.read_int(signed=False) # code
request_id = reader.read_long() request_id = reader.read_long()
inner_code = reader.read_int(signed=False) inner_code = reader.read_int(signed=False)
@ -417,11 +510,9 @@ class MtProtoSender:
request.confirm_received.set() request.confirm_received.set()
# else TODO Where should this error be reported? # else TODO Where should this error be reported?
# Read may be async. Can an error not-belong to a request? # Read may be async. Can an error not-belong to a request?
self._logger.debug('Read RPC error: %s', str(error))
return True # All contents were read okay return True # All contents were read okay
elif request: elif request:
self._logger.debug('Reading request response')
if inner_code == 0x3072cfa1: # GZip packed if inner_code == 0x3072cfa1: # GZip packed
unpacked_data = gzip.decompress(reader.tgread_bytes()) unpacked_data = gzip.decompress(reader.tgread_bytes())
with BinaryReader(unpacked_data) as compressed_reader: with BinaryReader(unpacked_data) as compressed_reader:
@ -436,11 +527,18 @@ class MtProtoSender:
# If it's really a result for RPC from previous connection # If it's really a result for RPC from previous connection
# session, it will be skipped by the handle_container() # session, it will be skipped by the handle_container()
self._logger.debug('Lost request will be skipped.') __log__.warning('Lost request will be skipped')
return False return False
async def _handle_gzip_packed(self, msg_id, sequence, reader, state): async def _handle_gzip_packed(self, msg_id, sequence, reader, state):
self._logger.debug('Handling gzip packed data') """
Handles a GzipPacked response.
:param msg_id: the ID of the message.
:param sequence: the sequence of the message.
:param reader: the reader containing the GzipPacked.
:return: the result of processing the packed message.
"""
with BinaryReader(GzipPacked.read(reader)) as compressed_reader: with BinaryReader(GzipPacked.read(reader)) as compressed_reader:
return await self._process_msg(msg_id, sequence, compressed_reader, state) return await self._process_msg(msg_id, sequence, compressed_reader, state)

View File

@ -36,10 +36,13 @@ from .update_state import UpdateState
from .utils import get_appropriated_part_size from .utils import get_appropriated_part_size
DEFAULT_DC_ID = 4
DEFAULT_IPV4_IP = '149.154.167.51' DEFAULT_IPV4_IP = '149.154.167.51'
DEFAULT_IPV6_IP = '[2001:67c:4e8:f002::a]' DEFAULT_IPV6_IP = '[2001:67c:4e8:f002::a]'
DEFAULT_PORT = 443 DEFAULT_PORT = 443
__log__ = logging.getLogger(__name__)
class TelegramBareClient: class TelegramBareClient:
"""Bare Telegram Client with just the minimum - """Bare Telegram Client with just the minimum -
@ -78,17 +81,17 @@ class TelegramBareClient:
**kwargs): **kwargs):
"""Refer to TelegramClient.__init__ for docs on this method""" """Refer to TelegramClient.__init__ for docs on this method"""
if not api_id or not api_hash: if not api_id or not api_hash:
raise PermissionError( raise ValueError(
"Your API ID or Hash cannot be empty or None. " "Your API ID or Hash cannot be empty or None. "
"Refer to Telethon's README.rst for more information.") "Refer to Telethon's wiki for more information.")
self._use_ipv6 = use_ipv6 self._use_ipv6 = use_ipv6
# Determine what session object we have # Determine what session object we have
if isinstance(session, str) or session is None: if isinstance(session, str) or session is None:
session = Session.try_load_or_create_new(session) session = Session(session)
elif not isinstance(session, Session): elif not isinstance(session, Session):
raise ValueError( raise TypeError(
'The given session must be a str or a Session instance.' 'The given session must be a str or a Session instance.'
) )
@ -97,9 +100,11 @@ class TelegramBareClient:
# ':' in session.server_address is True if it's an IPv6 address # ':' in session.server_address is True if it's an IPv6 address
if (not session.server_address or if (not session.server_address or
(':' in session.server_address) != use_ipv6): (':' in session.server_address) != use_ipv6):
session.port = DEFAULT_PORT session.set_dc(
session.server_address = \ DEFAULT_DC_ID,
DEFAULT_IPV6_IP if self._use_ipv6 else DEFAULT_IPV4_IP DEFAULT_IPV6_IP if self._use_ipv6 else DEFAULT_IPV4_IP,
DEFAULT_PORT
)
self.session = session self.session = session
self.api_id = int(api_id) self.api_id = int(api_id)
@ -117,10 +122,8 @@ class TelegramBareClient:
self._loop self._loop
) )
self._logger = logging.getLogger(__name__) # Two coroutines may be calling reconnect() when the connection
# is lost, we only want one to actually perform the reconnection.
# Two coroutines may be calling reconnect() when the connection is lost,
# we only want one to actually perform the reconnection.
self._reconnect_lock = Lock(loop=self._loop) self._reconnect_lock = Lock(loop=self._loop)
# Cache "exported" sessions as 'dc_id: Session' not to recreate # Cache "exported" sessions as 'dc_id: Session' not to recreate
@ -151,8 +154,9 @@ class TelegramBareClient:
# Save whether the user is authorized here (a.k.a. logged in) # Save whether the user is authorized here (a.k.a. logged in)
self._authorized = None # None = We don't know yet self._authorized = None # None = We don't know yet
# Uploaded files cache so subsequent calls are instant # The first request must be in invokeWithLayer(initConnection(X)).
self._upload_cache = {} # See https://core.telegram.org/api/invoking#saving-client-info.
self._first_request = True
self._recv_loop = None self._recv_loop = None
self._ping_loop = None self._ping_loop = None
@ -178,8 +182,12 @@ class TelegramBareClient:
native data center, raising a "UserMigrateError", and native data center, raising a "UserMigrateError", and
calling .disconnect() in the process. calling .disconnect() in the process.
""" """
__log__.info('Connecting to %s:%d...',
self.session.server_address, self.session.port)
try: try:
await self._sender.connect() await self._sender.connect()
__log__.info('Connection success!')
# Connection was successful! Try syncing the update state # Connection was successful! Try syncing the update state
# UNLESS '_sync_updates' is False (we probably are in # UNLESS '_sync_updates' is False (we probably are in
@ -199,14 +207,15 @@ class TelegramBareClient:
except TypeNotFoundError as e: except TypeNotFoundError as e:
# This is fine, probably layer migration # This is fine, probably layer migration
self._logger.debug('Found invalid item, probably migrating', e) __log__.warning('Connection failed, got unexpected type with ID '
'%s. Migrating?', hex(e.invalid_constructor_id))
self.disconnect() self.disconnect()
return await self.connect(_sync_updates=_sync_updates) return await self.connect(_sync_updates=_sync_updates)
except (RPCError, ConnectionError): except (RPCError, ConnectionError) as e:
# Probably errors from the previous session, ignore them # Probably errors from the previous session, ignore them
__log__.error('Connection failed due to %s', e)
self.disconnect() self.disconnect()
self._logger.exception('Could not stabilise initial connection.')
return False return False
def is_connected(self): def is_connected(self):
@ -227,10 +236,14 @@ class TelegramBareClient:
def disconnect(self): def disconnect(self):
"""Disconnects from the Telegram server""" """Disconnects from the Telegram server"""
__log__.info('Disconnecting...')
self._user_connected = False self._user_connected = False
self._sender.disconnect() self._sender.disconnect()
# TODO Shall we clear the _exported_sessions, or may be reused? # TODO Shall we clear the _exported_sessions, or may be reused?
pass self._first_request = True # On reconnect it will be first again
def __del__(self):
self.disconnect()
async def _reconnect(self, new_dc=None): async def _reconnect(self, new_dc=None):
"""If 'new_dc' is not set, only a call to .connect() will be made """If 'new_dc' is not set, only a call to .connect() will be made
@ -246,10 +259,13 @@ class TelegramBareClient:
try: try:
await self._reconnect_lock.acquire() await self._reconnect_lock.acquire()
if self.is_connected(): if self.is_connected():
__log__.info('Reconnection aborted: already connected')
return True return True
__log__.info('Attempting reconnection...')
return await self.connect() return await self.connect()
except ConnectionResetError: except ConnectionResetError:
__log__.warning('Reconnection failed due to %s', e)
return False return False
finally: finally:
self._reconnect_lock.release() self._reconnect_lock.release()
@ -258,9 +274,9 @@ class TelegramBareClient:
# we need to first know the Data Centers we can connect to. Do # we need to first know the Data Centers we can connect to. Do
# that before disconnecting. # that before disconnecting.
dc = await self._get_dc(new_dc) dc = await self._get_dc(new_dc)
__log__.info('Reconnecting to new data center %s', dc)
self.session.server_address = dc.ip_address self.session.set_dc(dc.id, dc.ip_address, dc.port)
self.session.port = dc.port
# auth_key's are associated with a server, which has now changed # auth_key's are associated with a server, which has now changed
# so it's not valid anymore. Set to None to force recreating it. # so it's not valid anymore. Set to None to force recreating it.
self.session.auth_key = None self.session.auth_key = None
@ -268,6 +284,13 @@ class TelegramBareClient:
self.disconnect() self.disconnect()
return await self.connect() return await self.connect()
def set_proxy(self, proxy):
"""Change the proxy used by the connections.
"""
if self.is_connected():
raise RuntimeError("You can't change the proxy while connected.")
self._sender.connection.conn.proxy = proxy
# endregion # endregion
# region Working with different connections/Data Centers # region Working with different connections/Data Centers
@ -315,6 +338,7 @@ class TelegramBareClient:
dc = await self._get_dc(dc_id) dc = await self._get_dc(dc_id)
# Export the current authorization to the new DC. # Export the current authorization to the new DC.
__log__.info('Exporting authorization for data center %s', dc)
export_auth = await self(ExportAuthorizationRequest(dc_id)) export_auth = await self(ExportAuthorizationRequest(dc_id))
# Create a temporary session for this IP address, which needs # Create a temporary session for this IP address, which needs
@ -323,10 +347,10 @@ class TelegramBareClient:
# Construct this session with the connection parameters # Construct this session with the connection parameters
# (system version, device model...) from the current one. # (system version, device model...) from the current one.
session = Session(self.session) session = Session(self.session)
session.server_address = dc.ip_address session.set_dc(dc.id, dc.ip_address, dc.port)
session.port = dc.port
self._exported_sessions[dc_id] = session self._exported_sessions[dc_id] = session
__log__.info('Creating exported new client')
client = TelegramBareClient( client = TelegramBareClient(
session, self.api_id, self.api_hash, session, self.api_id, self.api_hash,
proxy=self._sender.connection.conn.proxy, proxy=self._sender.connection.conn.proxy,
@ -339,7 +363,7 @@ class TelegramBareClient:
id=export_auth.id, bytes=export_auth.bytes id=export_auth.id, bytes=export_auth.bytes
)) ))
elif export_auth is not None: elif export_auth is not None:
self._logger.warning('Unknown return export_auth type', export_auth) __log__.warning('Unknown export auth type %s', export_auth)
client._authorized = True # We exported the auth, so we got auth client._authorized = True # We exported the auth, so we got auth
return client return client
@ -350,10 +374,10 @@ class TelegramBareClient:
if not session: if not session:
dc = await self._get_dc(cdn_redirect.dc_id, cdn=True) dc = await self._get_dc(cdn_redirect.dc_id, cdn=True)
session = Session(self.session) session = Session(self.session)
session.server_address = dc.ip_address session.set_dc(dc.id, dc.ip_address, dc.port)
session.port = dc.port
self._exported_sessions[cdn_redirect.dc_id] = session self._exported_sessions[cdn_redirect.dc_id] = session
__log__.info('Creating new CDN client')
client = TelegramBareClient( client = TelegramBareClient(
session, self.api_id, self.api_hash, session, self.api_id, self.api_hash,
proxy=self._sender.connection.conn.proxy, proxy=self._sender.connection.conn.proxy,
@ -378,11 +402,20 @@ class TelegramBareClient:
"""Invokes (sends) a MTProtoRequest and returns (receives) its result. """Invokes (sends) a MTProtoRequest and returns (receives) its result.
The invoke will be retried up to 'retries' times before raising The invoke will be retried up to 'retries' times before raising
ValueError(). RuntimeError().
""" """
if not all(isinstance(x, TLObject) and if not all(isinstance(x, TLObject) and
x.content_related for x in requests): x.content_related for x in requests):
raise ValueError('You can only invoke requests, not types!') raise TypeError('You can only invoke requests, not types!')
# For logging purposes
if len(requests) == 1:
which = type(requests[0]).__name__
else:
which = '{} requests ({})'.format(
len(requests), [type(x).__name__ for x in requests])
__log__.debug('Invoking %s', which)
# We should call receive from this thread if there's no background # We should call receive from this thread if there's no background
# thread reading or if the server disconnected us and we're trying # thread reading or if the server disconnected us and we're trying
@ -395,35 +428,34 @@ class TelegramBareClient:
if result is not None: if result is not None:
return result return result
__log__.warning('Invoking %s failed %d times, '
'reconnecting and retrying',
[str(x) for x in requests], retry + 1)
await asyncio.sleep(retry + 1, loop=self._loop) await asyncio.sleep(retry + 1, loop=self._loop)
self._logger.debug('RPC failed. Attempting reconnection.')
if not self._reconnect_lock.locked(): if not self._reconnect_lock.locked():
with await self._reconnect_lock: with await self._reconnect_lock:
self._reconnect() self._reconnect()
raise ValueError('Number of retries reached 0.') raise RuntimeError('Number of retries reached 0.')
# Let people use client.invoke(SomeRequest()) instead client(...) # Let people use client.invoke(SomeRequest()) instead client(...)
invoke = __call__ invoke = __call__
async def _invoke(self, call_receive, retry, *requests): async def _invoke(self, call_receive, retry, *requests):
# We need to specify the new layer (by initializing a new
# connection) if it has changed from the latest known one.
init_connection = self.session.layer != LAYER
try: try:
# Ensure that we start with no previous errors (i.e. resending) # Ensure that we start with no previous errors (i.e. resending)
for x in requests: for x in requests:
x.rpc_error = None x.rpc_error = None
if not self.session.auth_key: if not self.session.auth_key:
# New key, we need to tell the server we're going to use __log__.info('Need to generate new auth key before invoking')
# the latest layer and initialize the connection doing so. self._first_request = True
self.session.auth_key, self.session.time_offset = \ self.session.auth_key, self.session.time_offset = \
await authenticator.do_authentication(self._sender.connection) await authenticator.do_authentication(self._sender.connection)
init_connection = True
if init_connection: if self._first_request:
__log__.info('Initializing a new connection while invoking')
if len(requests) == 1: if len(requests) == 1:
requests = [self._wrap_init_connection(requests[0])] requests = [self._wrap_init_connection(requests[0])]
else: else:
@ -447,13 +479,14 @@ class TelegramBareClient:
await self._sender.receive(update_state=self.updates) await self._sender.receive(update_state=self.updates)
except BrokenAuthKeyError: except BrokenAuthKeyError:
self._logger.error('Broken auth key, a new one will be generated') __log__.error('Authorization key seems broken and was invalid!')
self.session.auth_key = None self.session.auth_key = None
except TimeoutError: except TimeoutError:
pass # We will just retry __log__.warning('Invoking timed out') # We will just retry
except ConnectionResetError: except ConnectionResetError:
__log__.warning('Connection was reset while invoking')
if self._user_connected: if self._user_connected:
# Server disconnected us, __call__ will try reconnecting. # Server disconnected us, __call__ will try reconnecting.
return None return None
@ -461,11 +494,8 @@ class TelegramBareClient:
# User never called .connect(), so raise this error. # User never called .connect(), so raise this error.
raise raise
if init_connection: # Clear the flag if we got this far
# We initialized the connection successfully, even if self._first_request = False
# a request had an RPC error we have invoked it fine.
self.session.layer = LAYER
self.session.save()
try: try:
raise next(x.rpc_error for x in requests if x.rpc_error) raise next(x.rpc_error for x in requests if x.rpc_error)
@ -482,27 +512,19 @@ class TelegramBareClient:
except (PhoneMigrateError, NetworkMigrateError, except (PhoneMigrateError, NetworkMigrateError,
UserMigrateError) as e: UserMigrateError) as e:
self._logger.debug(
'DC error when invoking request, '
'attempting to reconnect at DC {}'.format(e.new_dc)
)
await self._reconnect(new_dc=e.new_dc) await self._reconnect(new_dc=e.new_dc)
return None return None
except ServerError as e: except ServerError as e:
# Telegram is having some issues, just retry # Telegram is having some issues, just retry
self._logger.debug( __log__.error('Telegram servers are having internal errors %s', e)
'[ERROR] Telegram is having some internal issues', e
)
except FloodWaitError as e: except FloodWaitError as e:
__log__.warning('Request invoked too often, wait %ds', e.seconds)
if e.seconds > self.session.flood_sleep_threshold | 0: if e.seconds > self.session.flood_sleep_threshold | 0:
raise raise
self._logger.debug(
'Sleep of %d seconds below threshold, sleeping' % e.seconds
)
await asyncio.sleep(e.seconds, loop=self._loop) await asyncio.sleep(e.seconds, loop=self._loop)
return None return None
@ -540,6 +562,9 @@ class TelegramBareClient:
part_size_kb = get_appropriated_part_size(file_size) part_size_kb = get_appropriated_part_size(file_size)
file_name = os.path.basename(file_path) file_name = os.path.basename(file_path)
""" """
if isinstance(file, (InputFile, InputFileBig)):
return file # Already uploaded
if isinstance(file, str): if isinstance(file, str):
file_size = os.path.getsize(file) file_size = os.path.getsize(file)
elif isinstance(file, bytes): elif isinstance(file, bytes):
@ -548,6 +573,7 @@ class TelegramBareClient:
file = file.read() file = file.read()
file_size = len(file) file_size = len(file)
# File will now either be a string or bytes
if not part_size_kb: if not part_size_kb:
part_size_kb = get_appropriated_part_size(file_size) part_size_kb = get_appropriated_part_size(file_size)
@ -558,16 +584,40 @@ class TelegramBareClient:
if part_size % 1024 != 0: if part_size % 1024 != 0:
raise ValueError('The part size must be evenly divisible by 1024') raise ValueError('The part size must be evenly divisible by 1024')
# Set a default file name if None was specified
file_id = utils.generate_random_long()
if not file_name:
if isinstance(file, str):
file_name = os.path.basename(file)
else:
file_name = str(file_id)
# Determine whether the file is too big (over 10MB) or not # Determine whether the file is too big (over 10MB) or not
# Telegram does make a distinction between smaller or larger files # Telegram does make a distinction between smaller or larger files
is_large = file_size > 10 * 1024 * 1024 is_large = file_size > 10 * 1024 * 1024
if not is_large:
# Calculate the MD5 hash before anything else.
# As this needs to be done always for small files,
# might as well do it before anything else and
# check the cache.
if isinstance(file, str):
with open(file, 'rb') as stream:
file = stream.read()
hash_md5 = md5(file)
tuple_ = self.session.get_file(hash_md5.digest(), file_size)
if tuple_:
__log__.info('File was already cached, not uploading again')
return InputFile(name=file_name,
md5_checksum=tuple_[0], id=tuple_[2], parts=tuple_[3])
else:
hash_md5 = None
part_count = (file_size + part_size - 1) // part_size part_count = (file_size + part_size - 1) // part_size
__log__.info('Uploading file of %d bytes in %d chunks of %d',
file_size, part_count, part_size)
file_id = utils.generate_random_long() with open(file, 'rb') if isinstance(file, str) else BytesIO(file) \
hash_md5 = md5() as stream:
stream = open(file, 'rb') if isinstance(file, str) else BytesIO(file)
try:
for part_index in range(part_count): for part_index in range(part_count):
# Read the file by in chunks of size part_size # Read the file by in chunks of size part_size
part = stream.read(part_size) part = stream.read(part_size)
@ -582,28 +632,19 @@ class TelegramBareClient:
result = await self(request) result = await self(request)
if result: if result:
if not is_large: __log__.debug('Uploaded %d/%d', part_index + 1, part_count)
# No need to update the hash if it's a large file
hash_md5.update(part)
if progress_callback: if progress_callback:
progress_callback(stream.tell(), file_size) progress_callback(stream.tell(), file_size)
else: else:
raise ValueError('Failed to upload file part {}.' raise RuntimeError(
.format(part_index)) 'Failed to upload file part {}.'.format(part_index))
finally:
stream.close()
# Set a default file name if None was specified
if not file_name:
if isinstance(file, str):
file_name = os.path.basename(file)
else:
file_name = str(file_id)
if is_large: if is_large:
return InputFileBig(file_id, part_count, file_name) return InputFileBig(file_id, part_count, file_name)
else: else:
self.session.cache_file(
hash_md5.digest(), file_size, file_id, part_count)
return InputFile(file_id, part_count, file_name, return InputFile(file_id, part_count, file_name,
md5_checksum=hash_md5.hexdigest()) md5_checksum=hash_md5.hexdigest())
@ -650,6 +691,7 @@ class TelegramBareClient:
client = self client = self
cdn_decrypter = None cdn_decrypter = None
__log__.info('Downloading file in chunks of %d bytes', part_size)
try: try:
offset = 0 offset = 0
while True: while True:
@ -662,6 +704,7 @@ class TelegramBareClient:
)) ))
if isinstance(result, FileCdnRedirect): if isinstance(result, FileCdnRedirect):
__log__.info('File lives in a CDN')
cdn_decrypter, result = \ cdn_decrypter, result = \
await CdnDecrypter.prepare_decrypter( await CdnDecrypter.prepare_decrypter(
client, client,
@ -670,6 +713,7 @@ class TelegramBareClient:
) )
except FileMigrateError as e: except FileMigrateError as e:
__log__.info('File lives in another DC')
client = await self._get_exported_client(e.new_dc) client = await self._get_exported_client(e.new_dc)
continue continue
@ -682,6 +726,7 @@ class TelegramBareClient:
return getattr(result, 'type', '') return getattr(result, 'type', '')
f.write(result.bytes) f.write(result.bytes)
__log__.debug('Saved %d more bytes', len(result.bytes))
if progress_callback: if progress_callback:
progress_callback(f.tell(), file_size) progress_callback(f.tell(), file_size)
finally: finally:
@ -736,28 +781,30 @@ class TelegramBareClient:
self._ping_loop = None self._ping_loop = None
async def _recv_loop_impl(self): async def _recv_loop_impl(self):
__log__.info('Starting to wait for items from the network')
need_reconnect = False need_reconnect = False
while self._user_connected: while self._user_connected:
try: try:
if need_reconnect: if need_reconnect:
__log__.info('Attempting reconnection from read loop')
need_reconnect = False need_reconnect = False
while self._user_connected and not await self._reconnect(): while self._user_connected and not await self._reconnect():
# Retry forever, this is instant messaging # Retry forever, this is instant messaging
await asyncio.sleep(0.1, loop=self._loop) await asyncio.sleep(0.1, loop=self._loop)
__log__.debug('Receiving items from the network...')
await self._sender.receive(update_state=self.updates) await self._sender.receive(update_state=self.updates)
except TimeoutError: except TimeoutError:
# No problem. # No problem.
pass __log__.info('Receiving items from the network timed out')
except ConnectionError as error: except ConnectionError as error:
self._logger.debug(error)
need_reconnect = True need_reconnect = True
__log__.error('Connection was reset while receiving items')
await asyncio.sleep(1, loop=self._loop) await asyncio.sleep(1, loop=self._loop)
except Exception as error: except Exception as error:
# Unknown exception, pass it to the main thread # Unknown exception, pass it to the main thread
self._logger.exception( __log__.exception('Unknown exception in the read thread! '
'Unknown error on the read loop, please report.' 'Disconnecting and leaving it to main thread')
)
try: try:
import socks import socks

File diff suppressed because it is too large Load Diff

View File

@ -1 +1,2 @@
from .draft import Draft from .draft import Draft
from .dialog import Dialog

View File

@ -0,0 +1,37 @@
from . import Draft
from ... import utils
class Dialog:
"""
Custom class that encapsulates a dialog (an open "conversation" with
someone, a group or a channel) providing an abstraction to easily
access the input version/normal entity/message etc. The library will
return instances of this class when calling `client.get_dialogs()`.
"""
def __init__(self, client, dialog, entities, messages):
# Both entities and messages being dicts {ID: item}
self._client = client
self.dialog = dialog
self.pinned = bool(dialog.pinned)
self.message = messages.get(dialog.top_message, None)
self.date = getattr(self.message, 'date', None)
self.entity = entities[utils.get_peer_id(dialog.peer)]
self.input_entity = utils.get_input_peer(self.entity)
self.name = utils.get_display_name(self.entity)
self.unread_count = dialog.unread_count
self.unread_mentions_count = dialog.unread_mentions_count
if dialog.draft:
self.draft = Draft(client, dialog.peer, dialog.draft)
else:
self.draft = None
async def send_message(self, *args, **kwargs):
"""
Sends a message to this dialog. This is just a wrapper around
client.send_message(dialog.input_entity, *args, **kwargs).
"""
return await self._client.send_message(self.input_entity, *args, **kwargs)

View File

@ -21,7 +21,7 @@ class Draft:
@classmethod @classmethod
def _from_update(cls, client, update): def _from_update(cls, client, update):
if not isinstance(update, UpdateDraftMessage): if not isinstance(update, UpdateDraftMessage):
raise ValueError( raise TypeError(
'You can only create a new `Draft` from a corresponding ' 'You can only create a new `Draft` from a corresponding '
'`UpdateDraftMessage` object.' '`UpdateDraftMessage` object.'
) )
@ -29,14 +29,14 @@ class Draft:
return cls(client=client, peer=update.peer, draft=update.draft) return cls(client=client, peer=update.peer, draft=update.draft)
@property @property
def entity(self): async def entity(self):
return self._client.get_entity(self._peer) return await self._client.get_entity(self._peer)
@property @property
def input_entity(self): async def input_entity(self):
return self._client.get_input_entity(self._peer) return await self._client.get_input_entity(self._peer)
def set_message(self, text, no_webpage=None, reply_to_msg_id=None, entities=None): async def set_message(self, text, no_webpage=None, reply_to_msg_id=None, entities=None):
""" """
Changes the draft message on the Telegram servers. The changes are Changes the draft message on the Telegram servers. The changes are
reflected in this object. Changing only individual attributes like for reflected in this object. Changing only individual attributes like for
@ -56,7 +56,7 @@ class Draft:
:param list entities: A list of formatting entities :param list entities: A list of formatting entities
:return bool: `True` on success :return bool: `True` on success
""" """
result = self._client(SaveDraftRequest( result = await self._client(SaveDraftRequest(
peer=self._peer, peer=self._peer,
message=text, message=text,
no_webpage=no_webpage, no_webpage=no_webpage,
@ -72,9 +72,9 @@ class Draft:
return result return result
def delete(self): async def delete(self):
""" """
Deletes this draft Deletes this draft
:return bool: `True` on success :return bool: `True` on success
""" """
return self.set_message(text='') return await self.set_message(text='')

View File

@ -1,248 +0,0 @@
import re
from ..tl import TLObject
from ..tl.types import (
User, Chat, Channel, PeerUser, PeerChat, PeerChannel,
InputPeerUser, InputPeerChat, InputPeerChannel
)
from .. import utils # Keep this line the last to maybe fix #357
USERNAME_RE = re.compile(
r'@|(?:https?://)?(?:telegram\.(?:me|dog)|t\.me)/(joinchat/)?'
)
class EntityDatabase:
def __init__(self, input_list=None, enabled=True, enabled_full=True):
"""Creates a new entity database with an initial load of "Input"
entities, if any.
If 'enabled', input entities will be saved. The whole entity
will be saved if both 'enabled' and 'enabled_full' are True.
"""
self.enabled = enabled
self.enabled_full = enabled_full
self._entities = {} # marked_id: user|chat|channel
if input_list:
# TODO For compatibility reasons some sessions were saved with
# 'access_hash': null in the JSON session file. Drop these, as
# it means we don't have access to such InputPeers. Issue #354.
self._input_entities = {
k: v for k, v in input_list if v is not None
}
else:
self._input_entities = {} # marked_id: hash
# TODO Allow disabling some extra mappings
self._username_id = {} # username: marked_id
self._phone_id = {} # phone: marked_id
def process(self, tlobject):
"""Processes all the found entities on the given TLObject,
unless .enabled is False.
Returns True if new input entities were added.
"""
if not self.enabled:
return False
# Save all input entities we know of
if not isinstance(tlobject, TLObject) and hasattr(tlobject, '__iter__'):
# This may be a list of users already for instance
return self.expand(tlobject)
entities = []
if hasattr(tlobject, 'chats') and hasattr(tlobject.chats, '__iter__'):
entities.extend(tlobject.chats)
if hasattr(tlobject, 'users') and hasattr(tlobject.users, '__iter__'):
entities.extend(tlobject.users)
return self.expand(entities)
def expand(self, entities):
"""Adds new input entities to the local database unconditionally.
Unknown types will be ignored.
"""
if not entities or not self.enabled:
return False
new = [] # Array of entities (User, Chat, or Channel)
new_input = {} # Dictionary of {entity_marked_id: access_hash}
for e in entities:
if not isinstance(e, TLObject):
continue
try:
p = utils.get_input_peer(e, allow_self=False)
marked_id = utils.get_peer_id(p, add_mark=True)
has_hash = False
if isinstance(p, InputPeerChat):
# Chats don't have a hash
new_input[marked_id] = 0
has_hash = True
elif p.access_hash:
# Some users and channels seem to be returned without
# an 'access_hash', meaning Telegram doesn't want you
# to access them. This is the reason behind ensuring
# that the 'access_hash' is non-zero. See issue #354.
new_input[marked_id] = p.access_hash
has_hash = True
if self.enabled_full and has_hash:
if isinstance(e, (User, Chat, Channel)):
new.append(e)
except ValueError:
pass
before = len(self._input_entities)
self._input_entities.update(new_input)
for e in new:
self._add_full_entity(e)
return len(self._input_entities) != before
def _add_full_entity(self, entity):
"""Adds a "full" entity (User, Chat or Channel, not "Input*"),
despite the value of self.enabled and self.enabled_full.
Not to be confused with UserFull, ChatFull, or ChannelFull,
"full" means simply not "Input*".
"""
marked_id = utils.get_peer_id(
utils.get_input_peer(entity, allow_self=False), add_mark=True
)
try:
old_entity = self._entities[marked_id]
old_entity.__dict__.update(entity.__dict__) # Keep old references
# Update must delete old username and phone
username = getattr(old_entity, 'username', None)
if username:
del self._username_id[username.lower()]
phone = getattr(old_entity, 'phone', None)
if phone:
del self._phone_id[phone]
except KeyError:
# Add new entity
self._entities[marked_id] = entity
# Always update username or phone if any
username = getattr(entity, 'username', None)
if username:
self._username_id[username.lower()] = marked_id
phone = getattr(entity, 'phone', None)
if phone:
self._phone_id[phone] = marked_id
def _parse_key(self, key):
"""Parses the given string, integer or TLObject key into a
marked user ID ready for use on self._entities.
If a callable key is given, the entity will be passed to the
function, and if it returns a true-like value, the marked ID
for such entity will be returned.
Raises ValueError if it cannot be parsed.
"""
if isinstance(key, str):
phone = EntityDatabase.parse_phone(key)
try:
if phone:
return self._phone_id[phone]
else:
username, _ = EntityDatabase.parse_username(key)
return self._username_id[username.lower()]
except KeyError as e:
raise ValueError() from e
if isinstance(key, int):
return key # normal IDs are assumed users
if isinstance(key, TLObject):
return utils.get_peer_id(key, add_mark=True)
if callable(key):
for k, v in self._entities.items():
if key(v):
return k
raise ValueError()
def __getitem__(self, key):
"""See the ._parse_key() docstring for possible values of the key"""
try:
return self._entities[self._parse_key(key)]
except (ValueError, KeyError) as e:
raise KeyError(key) from e
def __delitem__(self, key):
try:
old = self._entities.pop(self._parse_key(key))
# Try removing the username and phone (if pop didn't fail),
# since the entity may have no username or phone, just ignore
# errors. It should be there if we popped the entity correctly.
try:
del self._username_id[getattr(old, 'username', None)]
except KeyError:
pass
try:
del self._phone_id[getattr(old, 'phone', None)]
except KeyError:
pass
except (ValueError, KeyError) as e:
raise KeyError(key) from e
@staticmethod
def parse_phone(phone):
"""Parses the given phone, or returns None if it's invalid"""
if isinstance(phone, int):
return str(phone)
else:
phone = re.sub(r'[+()\s-]', '', str(phone))
if phone.isdigit():
return phone
@staticmethod
def parse_username(username):
"""Parses the given username or channel access hash, given
a string, username or URL. Returns a tuple consisting of
both the stripped username and whether it is a joinchat/ hash.
"""
username = username.strip()
m = USERNAME_RE.match(username)
if m:
return username[m.end():], bool(m.group(1))
else:
return username, False
def get_input_entity(self, peer):
try:
i = utils.get_peer_id(peer, add_mark=True)
h = self._input_entities[i] # we store the IDs marked
i, k = utils.resolve_id(i) # removes the mark and returns kind
if k == PeerUser:
return InputPeerUser(i, h)
elif k == PeerChat:
return InputPeerChat(i)
elif k == PeerChannel:
return InputPeerChannel(i, h)
except ValueError as e:
raise KeyError(peer) from e
raise KeyError(peer)
def get_input_list(self):
return list(self._input_entities.items())
def clear(self, target=None):
if target is None:
self._entities.clear()
else:
del self[target]

View File

@ -1,12 +1,20 @@
import json import json
import os import os
import platform import platform
import sqlite3
import time import time
from base64 import b64encode, b64decode from base64 import b64decode
from os.path import isfile as file_exists from os.path import isfile as file_exists
from .entity_database import EntityDatabase from .. import utils, helpers
from .. import helpers from ..tl import TLObject
from ..tl.types import (
PeerUser, PeerChat, PeerChannel,
InputPeerUser, InputPeerChat, InputPeerChannel
)
EXTENSION = '.session'
CURRENT_VERSION = 2 # database version
class Session: class Session:
@ -17,33 +25,34 @@ class Session:
If you think the session has been compromised, close all the sessions If you think the session has been compromised, close all the sessions
through an official Telegram client to revoke the authorization. through an official Telegram client to revoke the authorization.
""" """
def __init__(self, session_user_id): def __init__(self, session_id):
"""session_user_id should either be a string or another Session. """session_user_id should either be a string or another Session.
Note that if another session is given, only parameters like Note that if another session is given, only parameters like
those required to init a connection will be copied. those required to init a connection will be copied.
""" """
# These values will NOT be saved # These values will NOT be saved
if isinstance(session_user_id, Session): self.filename = ':memory:'
self.session_user_id = None
# For connection purposes
session = session_user_id
self.device_model = session.device_model
self.system_version = session.system_version
self.app_version = session.app_version
self.lang_code = session.lang_code
self.system_lang_code = session.system_lang_code
self.lang_pack = session.lang_pack
self.report_errors = session.report_errors
self.save_entities = session.save_entities
self.flood_sleep_threshold = session.flood_sleep_threshold
# For connection purposes
if isinstance(session_id, Session):
self.device_model = session_id.device_model
self.system_version = session_id.system_version
self.app_version = session_id.app_version
self.lang_code = session_id.lang_code
self.system_lang_code = session_id.system_lang_code
self.lang_pack = session_id.lang_pack
self.report_errors = session_id.report_errors
self.save_entities = session_id.save_entities
self.flood_sleep_threshold = session_id.flood_sleep_threshold
else: # str / None else: # str / None
self.session_user_id = session_user_id if session_id:
self.filename = session_id
if not self.filename.endswith(EXTENSION):
self.filename += EXTENSION
system = platform.uname() system = platform.uname()
self.device_model = system.system if system.system else 'Unknown' self.device_model = system.system or 'Unknown'
self.system_version = system.release if system.release else '1.0' self.system_version = system.release or '1.0'
self.app_version = '1.0' # '0' will provoke error self.app_version = '1.0' # '0' will provoke error
self.lang_code = 'en' self.lang_code = 'en'
self.system_lang_code = self.lang_code self.system_lang_code = self.lang_code
@ -52,43 +61,177 @@ class Session:
self.save_entities = True self.save_entities = True
self.flood_sleep_threshold = 60 self.flood_sleep_threshold = 60
self.id = helpers.generate_random_long(signed=False) self.id = helpers.generate_random_long(signed=True)
self._sequence = 0 self._sequence = 0
self.time_offset = 0 self.time_offset = 0
self._last_msg_id = 0 # Long self._last_msg_id = 0 # Long
self.salt = 0 # Long
# These values will be saved # These values will be saved
self.server_address = None self._dc_id = 0
self.port = None self._server_address = None
self.auth_key = None self._port = None
self.layer = 0 self._auth_key = None
self.salt = 0 # Unsigned long
self.entities = EntityDatabase() # Known and cached entities # Migrating from .json -> SQL
entities = self._check_migrate_json()
self._conn = sqlite3.connect(self.filename, check_same_thread=False)
c = self._conn.cursor()
c.execute("select name from sqlite_master "
"where type='table' and name='version'")
if c.fetchone():
# Tables already exist, check for the version
c.execute("select version from version")
version = c.fetchone()[0]
if version != CURRENT_VERSION:
self._upgrade_database(old=version)
c.execute("delete from version")
c.execute("insert into version values (?)", (CURRENT_VERSION,))
self.save()
# These values will be saved
c.execute('select * from sessions')
tuple_ = c.fetchone()
if tuple_:
self._dc_id, self._server_address, self._port, key, = tuple_
from ..crypto import AuthKey
self._auth_key = AuthKey(data=key)
c.close()
else:
# Tables don't exist, create new ones
c.execute("create table version (version integer)")
c.execute("insert into version values (?)", (CURRENT_VERSION,))
c.execute(
"""create table sessions (
dc_id integer primary key,
server_address text,
port integer,
auth_key blob
) without rowid"""
)
c.execute(
"""create table entities (
id integer primary key,
hash integer not null,
username text,
phone integer,
name text
) without rowid"""
)
# Save file_size along with md5_digest
# to make collisions even more unlikely.
c.execute(
"""create table sent_files (
md5_digest blob,
file_size integer,
file_id integer,
part_count integer,
primary key(md5_digest, file_size)
) without rowid"""
)
# Migrating from JSON -> new table and may have entities
if entities:
c.executemany(
'insert or replace into entities values (?,?,?,?,?)',
entities
)
c.close()
self.save()
def _check_migrate_json(self):
if file_exists(self.filename):
try:
with open(self.filename, encoding='utf-8') as f:
data = json.load(f)
self.delete() # Delete JSON file to create database
self._port = data.get('port', self._port)
self._server_address = \
data.get('server_address', self._server_address)
from ..crypto import AuthKey
if data.get('auth_key_data', None) is not None:
key = b64decode(data['auth_key_data'])
self._auth_key = AuthKey(data=key)
rows = []
for p_id, p_hash in data.get('entities', []):
rows.append((p_id, p_hash, None, None, None))
return rows
except UnicodeDecodeError:
return [] # No entities
def _upgrade_database(self, old):
if old == 1:
self._conn.execute(
"""create table sent_files (
md5_digest blob,
file_size integer,
file_id integer,
part_count integer,
primary key(md5_digest, file_size)
) without rowid"""
)
old = 2
# Data from sessions should be kept as properties
# not to fetch the database every time we need it
def set_dc(self, dc_id, server_address, port):
self._dc_id = dc_id
self._server_address = server_address
self._port = port
self._update_session_table()
# Fetch the auth_key corresponding to this data center
c = self._conn.cursor()
c.execute('select auth_key from sessions')
tuple_ = c.fetchone()
if tuple_:
from ..crypto import AuthKey
self._auth_key = AuthKey(data=tuple_[0])
else:
self._auth_key = None
c.close()
@property
def server_address(self):
return self._server_address
@property
def port(self):
return self._port
@property
def auth_key(self):
return self._auth_key
@auth_key.setter
def auth_key(self, value):
self._auth_key = value
self._update_session_table()
def _update_session_table(self):
c = self._conn.cursor()
c.execute('insert or replace into sessions values (?,?,?,?)', (
self._dc_id,
self._server_address,
self._port,
self._auth_key.key if self._auth_key else b''
))
c.close()
def save(self): def save(self):
"""Saves the current session object as session_user_id.session""" """Saves the current session object as session_user_id.session"""
if not self.session_user_id: self._conn.commit()
return
with open('{}.session'.format(self.session_user_id), 'w') as file:
out_dict = {
'port': self.port,
'salt': self.salt,
'layer': self.layer,
'server_address': self.server_address,
'auth_key_data':
b64encode(self.auth_key.key).decode('ascii')
if self.auth_key else None
}
if self.save_entities:
out_dict['entities'] = self.entities.get_input_list()
json.dump(out_dict, file)
def delete(self): def delete(self):
"""Deletes the current session file""" """Deletes the current session file"""
if self.filename == ':memory:':
return True
try: try:
os.remove('{}.session'.format(self.session_user_id)) os.remove(self.filename)
return True return True
except OSError: except OSError:
return False return False
@ -99,43 +242,7 @@ class Session:
using this client and never logged out using this client and never logged out
""" """
return [os.path.splitext(os.path.basename(f))[0] return [os.path.splitext(os.path.basename(f))[0]
for f in os.listdir('.') if f.endswith('.session')] for f in os.listdir('.') if f.endswith(EXTENSION)]
@staticmethod
def try_load_or_create_new(session_user_id):
"""Loads a saved session_user_id.session or creates a new one.
If session_user_id=None, later .save()'s will have no effect.
"""
if session_user_id is None:
return Session(None)
else:
path = '{}.session'.format(session_user_id)
result = Session(session_user_id)
if not file_exists(path):
return result
try:
with open(path, 'r') as file:
data = json.load(file)
result.port = data.get('port', result.port)
result.salt = data.get('salt', result.salt)
result.layer = data.get('layer', result.layer)
result.server_address = \
data.get('server_address', result.server_address)
# FIXME We need to import the AuthKey here or otherwise
# we get cyclic dependencies.
from ..crypto import AuthKey
if data.get('auth_key_data', None) is not None:
key = b64decode(data['auth_key_data'])
result.auth_key = AuthKey(data=key)
result.entities = EntityDatabase(data.get('entities', []))
except (json.decoder.JSONDecodeError, UnicodeDecodeError):
pass
return result
def generate_sequence(self, content_related): def generate_sequence(self, content_related):
"""Thread safe method to generates the next sequence number, """Thread safe method to generates the next sequence number,
@ -173,9 +280,119 @@ class Session:
correct = correct_msg_id >> 32 correct = correct_msg_id >> 32
self.time_offset = correct - now self.time_offset = correct - now
def process_entities(self, tlobject): # Entity processing
try:
if self.entities.process(tlobject): def process_entities(self, tlo):
self.save() # Save if any new entities got added """Processes all the found entities on the given TLObject,
except: unless .enabled is False.
pass
Returns True if new input entities were added.
"""
if not self.save_entities:
return
if not isinstance(tlo, TLObject) and hasattr(tlo, '__iter__'):
# This may be a list of users already for instance
entities = tlo
else:
entities = []
if hasattr(tlo, 'chats') and hasattr(tlo.chats, '__iter__'):
entities.extend(tlo.chats)
if hasattr(tlo, 'users') and hasattr(tlo.users, '__iter__'):
entities.extend(tlo.users)
if not entities:
return
rows = [] # Rows to add (id, hash, username, phone, name)
for e in entities:
if not isinstance(e, TLObject):
continue
try:
p = utils.get_input_peer(e, allow_self=False)
marked_id = utils.get_peer_id(p)
except ValueError:
continue
p_hash = getattr(p, 'access_hash', 0)
if p_hash is None:
# Some users and channels seem to be returned without
# an 'access_hash', meaning Telegram doesn't want you
# to access them. This is the reason behind ensuring
# that the 'access_hash' is non-zero. See issue #354.
continue
username = getattr(e, 'username', None) or None
if username is not None:
username = username.lower()
phone = getattr(e, 'phone', None)
name = utils.get_display_name(e) or None
rows.append((marked_id, p_hash, username, phone, name))
if not rows:
return
self._conn.executemany(
'insert or replace into entities values (?,?,?,?,?)', rows
)
self.save()
def get_input_entity(self, key):
"""Parses the given string, integer or TLObject key into a
marked entity ID, which is then used to fetch the hash
from the database.
If a callable key is given, every row will be fetched,
and passed as a tuple to a function, that should return
a true-like value when the desired row is found.
Raises ValueError if it cannot be found.
"""
if isinstance(key, TLObject):
try:
# Try to early return if this key can be casted as input peer
return utils.get_input_peer(key)
except TypeError:
# Otherwise, get the ID of the peer
key = utils.get_peer_id(key)
c = self._conn.cursor()
if isinstance(key, str):
phone = utils.parse_phone(key)
if phone:
c.execute('select id, hash from entities where phone=?',
(phone,))
else:
username, _ = utils.parse_username(key)
c.execute('select id, hash from entities where username=?',
(username,))
if isinstance(key, int):
c.execute('select id, hash from entities where id=?', (key,))
result = c.fetchone()
c.close()
if result:
i, h = result # unpack resulting tuple
i, k = utils.resolve_id(i) # removes the mark and returns kind
if k == PeerUser:
return InputPeerUser(i, h)
elif k == PeerChat:
return InputPeerChat(i)
elif k == PeerChannel:
return InputPeerChannel(i, h)
else:
raise ValueError('Could not find input entity with key ', key)
# File processing
def get_file(self, md5_digest, file_size):
return self._conn.execute(
'select * from sent_files '
'where md5_digest = ? and file_size = ?', (md5_digest, file_size)
).fetchone()
def cache_file(self, md5_digest, file_size, file_id, part_count):
self._conn.execute(
'insert into sent_files values (?,?,?,?)',
(md5_digest, file_size, file_id, part_count)
)
self.save()

View File

@ -1,9 +1,9 @@
from datetime import datetime import struct
from datetime import datetime, date
class TLObject: class TLObject:
def __init__(self): def __init__(self):
self.request_msg_id = 0 # Long
self.confirm_received = None self.confirm_received = None
self.rpc_error = None self.rpc_error = None
@ -97,7 +97,8 @@ class TLObject:
if isinstance(data, str): if isinstance(data, str):
data = data.encode('utf-8') data = data.encode('utf-8')
else: else:
raise ValueError('bytes or str expected, not', type(data)) raise TypeError(
'bytes or str expected, not {}'.format(type(data)))
r = [] r = []
if len(data) < 254: if len(data) < 254:
@ -124,6 +125,23 @@ class TLObject:
r.append(bytes(padding)) r.append(bytes(padding))
return b''.join(r) return b''.join(r)
@staticmethod
def serialize_datetime(dt):
if not dt:
return b'\0\0\0\0'
if isinstance(dt, datetime):
dt = int(dt.timestamp())
elif isinstance(dt, date):
dt = int(datetime(dt.year, dt.month, dt.day).timestamp())
elif isinstance(dt, float):
dt = int(dt)
if isinstance(dt, int):
return struct.pack('<I', dt)
raise TypeError('Cannot interpret "{}" as a date.'.format(dt))
# These should be overrode # These should be overrode
def to_dict(self, recursive=True): def to_dict(self, recursive=True):
return {} return {}

View File

@ -6,6 +6,8 @@ from datetime import datetime
from .tl import types as tl from .tl import types as tl
__log__ = logging.getLogger(__name__)
class UpdateState: class UpdateState:
"""Used to hold the current state of processed updates. """Used to hold the current state of processed updates.
@ -32,11 +34,13 @@ class UpdateState:
the library itself. the library itself.
""" """
if isinstance(update, tl.updates.State): if isinstance(update, tl.updates.State):
__log__.debug('Saved new updates state')
self._state = update self._state = update
return # Nothing else to be done return # Nothing else to be done
pts = getattr(update, 'pts', self._state.pts) pts = getattr(update, 'pts', self._state.pts)
if hasattr(update, 'pts') and pts <= self._state.pts: if hasattr(update, 'pts') and pts <= self._state.pts:
__log__.info('Ignoring %s, already have it', update)
return # We already handled this update return # We already handled this update
self._state.pts = pts self._state.pts = pts
@ -57,28 +61,21 @@ class UpdateState:
""" """
data = pickle.dumps(update.to_dict()) data = pickle.dumps(update.to_dict())
if data in self._latest_updates: if data in self._latest_updates:
__log__.info('Ignoring %s, already have it', update)
return # Duplicated too return # Duplicated too
self._latest_updates.append(data) self._latest_updates.append(data)
if type(update).SUBCLASS_OF_ID == 0x8af52aac: # crc32(b'Updates') # Expand "Updates" into "Update", and pass these to callbacks.
# Expand "Updates" into "Update", and pass these to callbacks. # Since .users and .chats have already been processed, we
# Since .users and .chats have already been processed, we # don't need to care about those either.
# don't need to care about those either. if isinstance(update, tl.UpdateShort):
if isinstance(update, tl.UpdateShort): self.handle_update(update.update)
self.handle_update(update.update)
elif isinstance(update, (tl.Updates, tl.UpdatesCombined)): elif isinstance(update, (tl.Updates, tl.UpdatesCombined)):
for upd in update.updates: for upd in update.updates:
self.handle_update(upd) self.handle_update(upd)
elif not isinstance(update, tl.UpdatesTooLong): # TODO Handle "Updates too long"
# TODO Handle "Updates too long"
self.handle_update(update)
elif type(update).SUBCLASS_OF_ID == 0x9f89304e: # crc32(b'Update')
self.handle_update(update)
else: else:
self._logger.debug('Ignoring "update" of type {}'.format( self.handle_update(update)
type(update).__name__)
)

View File

@ -5,6 +5,8 @@ to convert between an entity like an User, Chat, etc. into its Input version)
import math import math
from mimetypes import add_type, guess_extension from mimetypes import add_type, guess_extension
import re
from .tl import TLObject from .tl import TLObject
from .tl.types import ( from .tl.types import (
Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull, Channel, ChannelForbidden, Chat, ChatEmpty, ChatForbidden, ChatFull,
@ -24,6 +26,11 @@ from .tl.types import (
) )
USERNAME_RE = re.compile(
r'@|(?:https?://)?(?:telegram\.(?:me|dog)|t\.me)/(joinchat/)?'
)
def get_display_name(entity): def get_display_name(entity):
"""Gets the input peer for the given "entity" (user, chat or channel) """Gets the input peer for the given "entity" (user, chat or channel)
Returns None if it was not found""" Returns None if it was not found"""
@ -35,12 +42,12 @@ def get_display_name(entity):
elif entity.last_name: elif entity.last_name:
return entity.last_name return entity.last_name
else: else:
return '(No name)' return ''
if isinstance(entity, (Chat, Channel)): elif isinstance(entity, (Chat, Channel)):
return entity.title return entity.title
return '(unknown)' return ''
# For some reason, .webp (stickers' format) is not registered # For some reason, .webp (stickers' format) is not registered
add_type('image/webp', '.webp') add_type('image/webp', '.webp')
@ -67,13 +74,13 @@ def get_extension(media):
def _raise_cast_fail(entity, target): def _raise_cast_fail(entity, target):
raise ValueError('Cannot cast {} to any kind of {}.' raise TypeError('Cannot cast {} to any kind of {}.'.format(
.format(type(entity).__name__, target)) type(entity).__name__, target))
def get_input_peer(entity, allow_self=True): def get_input_peer(entity, allow_self=True):
"""Gets the input peer for the given "entity" (user, chat or channel). """Gets the input peer for the given "entity" (user, chat or channel).
A ValueError is raised if the given entity isn't a supported type.""" A TypeError is raised if the given entity isn't a supported type."""
if not isinstance(entity, TLObject): if not isinstance(entity, TLObject):
_raise_cast_fail(entity, 'InputPeer') _raise_cast_fail(entity, 'InputPeer')
@ -305,9 +312,40 @@ def get_input_media(media, user_caption=None, is_photo=False):
_raise_cast_fail(media, 'InputMedia') _raise_cast_fail(media, 'InputMedia')
def get_peer_id(peer, add_mark=False): def parse_phone(phone):
"""Finds the ID of the given peer, and optionally converts it to """Parses the given phone, or returns None if it's invalid"""
the "bot api" format if 'add_mark' is set to True. if isinstance(phone, int):
return str(phone)
else:
phone = re.sub(r'[+()\s-]', '', str(phone))
if phone.isdigit():
return phone
def parse_username(username):
"""Parses the given username or channel access hash, given
a string, username or URL. Returns a tuple consisting of
both the stripped, lowercase username and whether it is
a joinchat/ hash (in which case is not lowercase'd).
"""
username = username.strip()
m = USERNAME_RE.match(username)
if m:
result = username[m.end():]
is_invite = bool(m.group(1))
return result if is_invite else result.lower(), is_invite
else:
return username.lower(), False
def get_peer_id(peer):
"""
Finds the ID of the given peer, and converts it to the "bot api" format
so it the peer can be identified back. User ID is left unmodified,
chat ID is negated, and channel ID is prefixed with -100.
The original ID and the peer type class can be returned with
a call to utils.resolve_id(marked_id).
""" """
# First we assert it's a Peer TLObject, or early return for integers # First we assert it's a Peer TLObject, or early return for integers
if not isinstance(peer, TLObject): if not isinstance(peer, TLObject):
@ -324,7 +362,7 @@ def get_peer_id(peer, add_mark=False):
if isinstance(peer, (PeerUser, InputPeerUser)): if isinstance(peer, (PeerUser, InputPeerUser)):
return peer.user_id return peer.user_id
elif isinstance(peer, (PeerChat, InputPeerChat)): elif isinstance(peer, (PeerChat, InputPeerChat)):
return -peer.chat_id if add_mark else peer.chat_id return -peer.chat_id
elif isinstance(peer, (PeerChannel, InputPeerChannel, ChannelFull)): elif isinstance(peer, (PeerChannel, InputPeerChannel, ChannelFull)):
if isinstance(peer, ChannelFull): if isinstance(peer, ChannelFull):
# Special case: .get_input_peer can't return InputChannel from # Special case: .get_input_peer can't return InputChannel from
@ -332,12 +370,9 @@ def get_peer_id(peer, add_mark=False):
i = peer.id i = peer.id
else: else:
i = peer.channel_id i = peer.channel_id
if add_mark: # Concat -100 through math tricks, .to_supergroup() on Madeline
# Concat -100 through math tricks, .to_supergroup() on Madeline # IDs will be strictly positive -> log works
# IDs will be strictly positive -> log works return -(i + pow(10, math.floor(math.log10(i) + 3)))
return -(i + pow(10, math.floor(math.log10(i) + 3)))
else:
return i
_raise_cast_fail(peer, 'int') _raise_cast_fail(peer, 'int')
@ -353,28 +388,6 @@ def resolve_id(marked_id):
return -marked_id, PeerChat return -marked_id, PeerChat
def find_user_or_chat(peer, users, chats):
"""Finds the corresponding user or chat given a peer.
Returns None if it was not found"""
if isinstance(peer, PeerUser):
peer, where = peer.user_id, users
else:
where = chats
if isinstance(peer, PeerChat):
peer = peer.chat_id
elif isinstance(peer, PeerChannel):
peer = peer.channel_id
if isinstance(peer, int):
if isinstance(where, dict):
return where.get(peer)
else:
try:
return next(x for x in where if x.id == peer)
except StopIteration:
pass
def get_appropriated_part_size(file_size): def get_appropriated_part_size(file_size):
"""Gets the appropriated part size when uploading or downloading files, """Gets the appropriated part size when uploading or downloading files,
given an initial file size""" given an initial file size"""

View File

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

View File

@ -138,15 +138,15 @@ class InteractiveTelegramClient(TelegramClient):
# Entities represent the user, chat or channel # Entities represent the user, chat or channel
# corresponding to the dialog on the same index. # corresponding to the dialog on the same index.
dialogs, entities = self.get_dialogs(limit=dialog_count) dialogs = self.get_dialogs(limit=dialog_count)
i = None i = None
while i is None: while i is None:
print_title('Dialogs window') print_title('Dialogs window')
# Display them so the user can choose # Display them so the user can choose
for i, entity in enumerate(entities, start=1): for i, dialog in enumerate(dialogs, start=1):
sprint('{}. {}'.format(i, get_display_name(entity))) sprint('{}. {}'.format(i, get_display_name(dialog.entity)))
# Let the user decide who they want to talk to # Let the user decide who they want to talk to
print() print()
@ -177,7 +177,7 @@ class InteractiveTelegramClient(TelegramClient):
i = None i = None
# Retrieve the selected user (or chat, or channel) # Retrieve the selected user (or chat, or channel)
entity = entities[i] entity = dialogs[i].entity
# Show some information # Show some information
print_title('Chat with "{}"'.format(get_display_name(entity))) print_title('Chat with "{}"'.format(get_display_name(entity)))

View File

@ -17,11 +17,13 @@ class TLParser:
# Read all the lines from the .tl file # Read all the lines from the .tl file
for line in file: for line in file:
# Strip comments from the line
comment_index = line.find('//')
if comment_index != -1:
line = line[:comment_index]
line = line.strip() line = line.strip()
if line:
# Ensure that the line is not a comment
if line and not line.startswith('//'):
# Check whether the line is a type change # Check whether the line is a type change
# (types <-> functions) or not # (types <-> functions) or not
match = re.match('---(\w+)---', line) match = re.match('---(\w+)---', line)

View File

@ -166,11 +166,9 @@ inputMediaGifExternal#4843b0fd url:string q:string = InputMedia;
inputMediaPhotoExternal#922aec1 flags:# url:string caption:string ttl_seconds:flags.0?int = InputMedia; inputMediaPhotoExternal#922aec1 flags:# url:string caption:string ttl_seconds:flags.0?int = InputMedia;
inputMediaDocumentExternal#b6f74335 flags:# url:string caption:string ttl_seconds:flags.0?int = InputMedia; inputMediaDocumentExternal#b6f74335 flags:# url:string caption:string ttl_seconds:flags.0?int = InputMedia;
inputMediaGame#d33f43f3 id:InputGame = InputMedia; inputMediaGame#d33f43f3 id:InputGame = InputMedia;
inputMediaInvoice#92153685 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string start_param:string = InputMedia; inputMediaInvoice#f4e096c3 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:string = InputMedia;
inputMediaGeoLive#7b1a118f geo_point:InputGeoPoint period:int = InputMedia; inputMediaGeoLive#7b1a118f geo_point:InputGeoPoint period:int = InputMedia;
inputSingleMedia#5eaa7809 media:InputMedia random_id:long = InputSingleMedia;
inputChatPhotoEmpty#1ca48f57 = InputChatPhoto; inputChatPhotoEmpty#1ca48f57 = InputChatPhoto;
inputChatUploadedPhoto#927c55b4 file:InputFile = InputChatPhoto; inputChatUploadedPhoto#927c55b4 file:InputFile = InputChatPhoto;
inputChatPhoto#8953ad37 id:InputPhoto = InputChatPhoto; inputChatPhoto#8953ad37 id:InputPhoto = InputChatPhoto;
@ -345,6 +343,7 @@ messages.dialogsSlice#71e094f3 count:int dialogs:Vector<Dialog> messages:Vector<
messages.messages#8c718e87 messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.Messages; messages.messages#8c718e87 messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.Messages;
messages.messagesSlice#b446ae3 count:int messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.Messages; messages.messagesSlice#b446ae3 count:int messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.Messages;
messages.channelMessages#99262e37 flags:# pts:int count:int messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.Messages; messages.channelMessages#99262e37 flags:# pts:int count:int messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.Messages;
messages.messagesNotModified#74535f21 count:int = messages.Messages;
messages.chats#64ff9fd5 chats:Vector<Chat> = messages.Chats; messages.chats#64ff9fd5 chats:Vector<Chat> = messages.Chats;
messages.chatsSlice#9cd81144 count:int chats:Vector<Chat> = messages.Chats; messages.chatsSlice#9cd81144 count:int chats:Vector<Chat> = messages.Chats;
@ -357,7 +356,6 @@ inputMessagesFilterEmpty#57e2f66c = MessagesFilter;
inputMessagesFilterPhotos#9609a51c = MessagesFilter; inputMessagesFilterPhotos#9609a51c = MessagesFilter;
inputMessagesFilterVideo#9fc00e65 = MessagesFilter; inputMessagesFilterVideo#9fc00e65 = MessagesFilter;
inputMessagesFilterPhotoVideo#56e9f0e4 = MessagesFilter; inputMessagesFilterPhotoVideo#56e9f0e4 = MessagesFilter;
inputMessagesFilterPhotoVideoDocuments#d95e73bb = MessagesFilter;
inputMessagesFilterDocument#9eddf188 = MessagesFilter; inputMessagesFilterDocument#9eddf188 = MessagesFilter;
inputMessagesFilterUrl#7ef0dd87 = MessagesFilter; inputMessagesFilterUrl#7ef0dd87 = MessagesFilter;
inputMessagesFilterGif#ffc86587 = MessagesFilter; inputMessagesFilterGif#ffc86587 = MessagesFilter;
@ -368,8 +366,8 @@ inputMessagesFilterPhoneCalls#80c99768 flags:# missed:flags.0?true = MessagesFil
inputMessagesFilterRoundVoice#7a7c17a4 = MessagesFilter; inputMessagesFilterRoundVoice#7a7c17a4 = MessagesFilter;
inputMessagesFilterRoundVideo#b549da53 = MessagesFilter; inputMessagesFilterRoundVideo#b549da53 = MessagesFilter;
inputMessagesFilterMyMentions#c1f8e69a = MessagesFilter; inputMessagesFilterMyMentions#c1f8e69a = MessagesFilter;
inputMessagesFilterContacts#e062db83 = MessagesFilter;
inputMessagesFilterGeo#e7026d0d = MessagesFilter; inputMessagesFilterGeo#e7026d0d = MessagesFilter;
inputMessagesFilterContacts#e062db83 = MessagesFilter;
updateNewMessage#1f2b0afd message:Message pts:int pts_count:int = Update; updateNewMessage#1f2b0afd message:Message pts:int pts_count:int = Update;
updateMessageID#4e90bfd6 id:int random_id:long = Update; updateMessageID#4e90bfd6 id:int random_id:long = Update;
@ -463,7 +461,7 @@ upload.fileCdnRedirect#ea52fe5a dc_id:int file_token:bytes encryption_key:bytes
dcOption#5d8c6cc flags:# ipv6:flags.0?true media_only:flags.1?true tcpo_only:flags.2?true cdn:flags.3?true static:flags.4?true id:int ip_address:string port:int = DcOption; dcOption#5d8c6cc flags:# ipv6:flags.0?true media_only:flags.1?true tcpo_only:flags.2?true cdn:flags.3?true static:flags.4?true id:int ip_address:string port:int = DcOption;
config#9c840964 flags:# phonecalls_enabled:flags.1?true date:int expires:int test_mode:Bool this_dc:int dc_options:Vector<DcOption> chat_size_max:int megagroup_size_max:int forwarded_count_max:int online_update_period_ms:int offline_blur_timeout_ms:int offline_idle_timeout_ms:int online_cloud_timeout_ms:int notify_cloud_delay_ms:int notify_default_delay_ms:int chat_big_size:int push_chat_period_ms:int push_chat_limit:int saved_gifs_limit:int edit_time_limit:int rating_e_decay:int stickers_recent_limit:int stickers_faved_limit:int channels_read_media_period:int tmp_sessions:flags.0?int pinned_dialogs_count_max:int call_receive_timeout_ms:int call_ring_timeout_ms:int call_connect_timeout_ms:int call_packet_timeout_ms:int me_url_prefix:string suggested_lang_code:flags.2?string lang_pack_version:flags.2?int disabled_features:Vector<DisabledFeature> = Config; config#9c840964 flags:# phonecalls_enabled:flags.1?true default_p2p_contacts:flags.3?true date:int expires:int test_mode:Bool this_dc:int dc_options:Vector<DcOption> chat_size_max:int megagroup_size_max:int forwarded_count_max:int online_update_period_ms:int offline_blur_timeout_ms:int offline_idle_timeout_ms:int online_cloud_timeout_ms:int notify_cloud_delay_ms:int notify_default_delay_ms:int chat_big_size:int push_chat_period_ms:int push_chat_limit:int saved_gifs_limit:int edit_time_limit:int rating_e_decay:int stickers_recent_limit:int stickers_faved_limit:int channels_read_media_period:int tmp_sessions:flags.0?int pinned_dialogs_count_max:int call_receive_timeout_ms:int call_ring_timeout_ms:int call_connect_timeout_ms:int call_packet_timeout_ms:int me_url_prefix:string suggested_lang_code:flags.2?string lang_pack_version:flags.2?int disabled_features:Vector<DisabledFeature> = Config;
nearestDc#8e1a1775 country:string this_dc:int nearest_dc:int = NearestDc; nearestDc#8e1a1775 country:string this_dc:int nearest_dc:int = NearestDc;
@ -524,7 +522,7 @@ sendMessageGamePlayAction#dd6a8f48 = SendMessageAction;
sendMessageRecordRoundAction#88f27fbc = SendMessageAction; sendMessageRecordRoundAction#88f27fbc = SendMessageAction;
sendMessageUploadRoundAction#243e1c66 progress:int = SendMessageAction; sendMessageUploadRoundAction#243e1c66 progress:int = SendMessageAction;
contacts.found#1aa1f784 results:Vector<Peer> chats:Vector<Chat> users:Vector<User> = contacts.Found; contacts.found#b3134d9d my_results:Vector<Peer> results:Vector<Peer> chats:Vector<Chat> users:Vector<User> = contacts.Found;
inputPrivacyKeyStatusTimestamp#4f96cb18 = InputPrivacyKey; inputPrivacyKeyStatusTimestamp#4f96cb18 = InputPrivacyKey;
inputPrivacyKeyChatInvite#bdfb0426 = InputPrivacyKey; inputPrivacyKeyChatInvite#bdfb0426 = InputPrivacyKey;
@ -723,7 +721,7 @@ auth.sentCodeTypeSms#c000bba2 length:int = auth.SentCodeType;
auth.sentCodeTypeCall#5353e5a7 length:int = auth.SentCodeType; auth.sentCodeTypeCall#5353e5a7 length:int = auth.SentCodeType;
auth.sentCodeTypeFlashCall#ab03c6d9 pattern:string = auth.SentCodeType; auth.sentCodeTypeFlashCall#ab03c6d9 pattern:string = auth.SentCodeType;
messages.botCallbackAnswer#36585ea4 flags:# alert:flags.1?true has_url:flags.3?true message:flags.0?string url:flags.2?string cache_time:int = messages.BotCallbackAnswer; messages.botCallbackAnswer#36585ea4 flags:# alert:flags.1?true has_url:flags.3?true native_ui:flags.4?true message:flags.0?string url:flags.2?string cache_time:int = messages.BotCallbackAnswer;
messages.messageEditData#26b5dde6 flags:# caption:flags.0?true = messages.MessageEditData; messages.messageEditData#26b5dde6 flags:# caption:flags.0?true = messages.MessageEditData;
@ -825,7 +823,7 @@ dataJSON#7d748d04 data:string = DataJSON;
labeledPrice#cb296bf8 label:string amount:long = LabeledPrice; labeledPrice#cb296bf8 label:string amount:long = LabeledPrice;
invoice#c30aa358 flags:# test:flags.0?true name_requested:flags.1?true phone_requested:flags.2?true email_requested:flags.3?true shipping_address_requested:flags.4?true flexible:flags.5?true currency:string prices:Vector<LabeledPrice> = Invoice; invoice#c30aa358 flags:# test:flags.0?true name_requested:flags.1?true phone_requested:flags.2?true email_requested:flags.3?true shipping_address_requested:flags.4?true flexible:flags.5?true phone_to_provider:flags.6?true email_to_provider:flags.7?true currency:string prices:Vector<LabeledPrice> = Invoice;
paymentCharge#ea02c27e id:string provider_charge_id:string = PaymentCharge; paymentCharge#ea02c27e id:string provider_charge_id:string = PaymentCharge;
@ -856,6 +854,8 @@ payments.savedInfo#fb8fe43c flags:# has_saved_credentials:flags.1?true saved_inf
inputPaymentCredentialsSaved#c10eb2cf id:string tmp_password:bytes = InputPaymentCredentials; inputPaymentCredentialsSaved#c10eb2cf id:string tmp_password:bytes = InputPaymentCredentials;
inputPaymentCredentials#3417d728 flags:# save:flags.0?true data:DataJSON = InputPaymentCredentials; inputPaymentCredentials#3417d728 flags:# save:flags.0?true data:DataJSON = InputPaymentCredentials;
inputPaymentCredentialsApplePay#aa1c39f payment_data:DataJSON = InputPaymentCredentials;
inputPaymentCredentialsAndroidPay#ca05d50e payment_token:DataJSON google_transaction_id:string = InputPaymentCredentials;
account.tmpPassword#db64fd34 tmp_password:bytes valid_until:int = account.TmpPassword; account.tmpPassword#db64fd34 tmp_password:bytes valid_until:int = account.TmpPassword;
@ -893,7 +893,7 @@ langPackDifference#f385c1f6 lang_code:string from_version:int version:int string
langPackLanguage#117698f1 name:string native_name:string lang_code:string = LangPackLanguage; langPackLanguage#117698f1 name:string native_name:string lang_code:string = LangPackLanguage;
channelAdminRights#5d7ceba5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true invite_link:flags.6?true pin_messages:flags.7?true add_admins:flags.9?true = ChannelAdminRights; channelAdminRights#5d7ceba5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true invite_link:flags.6?true pin_messages:flags.7?true add_admins:flags.9?true manage_call:flags.10?true = ChannelAdminRights;
channelBannedRights#58cf4249 flags:# view_messages:flags.0?true send_messages:flags.1?true send_media:flags.2?true send_stickers:flags.3?true send_gifs:flags.4?true send_games:flags.5?true send_inline:flags.6?true embed_links:flags.7?true until_date:int = ChannelBannedRights; channelBannedRights#58cf4249 flags:# view_messages:flags.0?true send_messages:flags.1?true send_media:flags.2?true send_stickers:flags.3?true send_gifs:flags.4?true send_games:flags.5?true send_inline:flags.6?true embed_links:flags.7?true until_date:int = ChannelBannedRights;
@ -927,13 +927,15 @@ cdnFileHash#77eec38f offset:int limit:int hash:bytes = CdnFileHash;
messages.favedStickersNotModified#9e8fa6d3 = messages.FavedStickers; messages.favedStickersNotModified#9e8fa6d3 = messages.FavedStickers;
messages.favedStickers#f37f2f16 hash:int packs:Vector<StickerPack> stickers:Vector<Document> = messages.FavedStickers; messages.favedStickers#f37f2f16 hash:int packs:Vector<StickerPack> stickers:Vector<Document> = messages.FavedStickers;
help.recentMeUrls#e0310d7 urls:Vector<RecentMeUrl> chats:Vector<Chat> users:Vector<User> = help.RecentMeUrls; recentMeUrlUnknown#46e1d13d url:string = RecentMeUrl;
recentMeUrlUser#8dbc3336 url:string user_id:int = RecentMeUrl; recentMeUrlUser#8dbc3336 url:string user_id:int = RecentMeUrl;
recentMeUrlChat#a01b22f9 url:string chat_id:int = RecentMeUrl; recentMeUrlChat#a01b22f9 url:string chat_id:int = RecentMeUrl;
recentMeUrlStickerSet#bc0a57dc url:string set:StickerSetCovered = RecentMeUrl;
recentMeUrlChatInvite#eb49081d url:string chat_invite:ChatInvite = RecentMeUrl; recentMeUrlChatInvite#eb49081d url:string chat_invite:ChatInvite = RecentMeUrl;
recentMeUrlUnknown#46e1d13d url:string = RecentMeUrl; recentMeUrlStickerSet#bc0a57dc url:string set:StickerSetCovered = RecentMeUrl;
help.recentMeUrls#e0310d7 urls:Vector<RecentMeUrl> chats:Vector<Chat> users:Vector<User> = help.RecentMeUrls;
inputSingleMedia#5eaa7809 media:InputMedia random_id:long = InputSingleMedia;
---functions--- ---functions---
@ -961,8 +963,8 @@ auth.resendCode#3ef1a9bf phone_number:string phone_code_hash:string = auth.SentC
auth.cancelCode#1f040578 phone_number:string phone_code_hash:string = Bool; auth.cancelCode#1f040578 phone_number:string phone_code_hash:string = Bool;
auth.dropTempAuthKeys#8e48a188 except_auth_keys:Vector<long> = Bool; auth.dropTempAuthKeys#8e48a188 except_auth_keys:Vector<long> = Bool;
account.registerDevice#637ea878 token_type:int token:string = Bool; account.registerDevice#f75874d1 token_type:int token:string other_uids:Vector<int> = Bool;
account.unregisterDevice#65c55b40 token_type:int token:string = Bool; account.unregisterDevice#3076c4bf token_type:int token:string other_uids:Vector<int> = Bool;
account.updateNotifySettings#84be5b93 peer:InputNotifyPeer settings:InputPeerNotifySettings = Bool; account.updateNotifySettings#84be5b93 peer:InputNotifyPeer settings:InputPeerNotifySettings = Bool;
account.getNotifySettings#12b3ad31 peer:InputNotifyPeer = PeerNotifySettings; account.getNotifySettings#12b3ad31 peer:InputNotifyPeer = PeerNotifySettings;
account.resetNotifySettings#db7e1747 = Bool; account.resetNotifySettings#db7e1747 = Bool;
@ -1010,7 +1012,7 @@ contacts.resetSaved#879537f1 = Bool;
messages.getMessages#4222fa74 id:Vector<int> = messages.Messages; messages.getMessages#4222fa74 id:Vector<int> = messages.Messages;
messages.getDialogs#191ba9c5 flags:# exclude_pinned:flags.0?true offset_date:int offset_id:int offset_peer:InputPeer limit:int = messages.Dialogs; messages.getDialogs#191ba9c5 flags:# exclude_pinned:flags.0?true offset_date:int offset_id:int offset_peer:InputPeer limit:int = messages.Dialogs;
messages.getHistory#afa92846 peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; messages.getHistory#dcbb8260 peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:int = messages.Messages;
messages.search#39e9ea0 flags:# peer:InputPeer q:string from_id:flags.0?InputUser filter:MessagesFilter min_date:int max_date:int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; messages.search#39e9ea0 flags:# peer:InputPeer q:string from_id:flags.0?InputUser filter:MessagesFilter min_date:int max_date:int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages;
messages.readHistory#e306d3a peer:InputPeer max_id:int = messages.AffectedMessages; messages.readHistory#e306d3a peer:InputPeer max_id:int = messages.AffectedMessages;
messages.deleteHistory#1c015b09 flags:# just_clear:flags.0?true peer:InputPeer max_id:int = messages.AffectedHistory; messages.deleteHistory#1c015b09 flags:# just_clear:flags.0?true peer:InputPeer max_id:int = messages.AffectedHistory;
@ -1067,7 +1069,7 @@ messages.setInlineBotResults#eb5ea206 flags:# gallery:flags.0?true private:flags
messages.sendInlineBotResult#b16e06fe flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int random_id:long query_id:long id:string = Updates; messages.sendInlineBotResult#b16e06fe flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int random_id:long query_id:long id:string = Updates;
messages.getMessageEditData#fda68d36 peer:InputPeer id:int = messages.MessageEditData; messages.getMessageEditData#fda68d36 peer:InputPeer id:int = messages.MessageEditData;
messages.editMessage#5d1b8dd flags:# no_webpage:flags.1?true stop_geo_live:flags.12?true peer:InputPeer id:int message:flags.11?string reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> geo_point:flags.13?InputGeoPoint = Updates; messages.editMessage#5d1b8dd flags:# no_webpage:flags.1?true stop_geo_live:flags.12?true peer:InputPeer id:int message:flags.11?string reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> geo_point:flags.13?InputGeoPoint = Updates;
messages.editInlineBotMessage#130c2c85 flags:# no_webpage:flags.1?true id:InputBotInlineMessageID message:flags.11?string reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> = Bool; messages.editInlineBotMessage#b0e08243 flags:# no_webpage:flags.1?true stop_geo_live:flags.12?true id:InputBotInlineMessageID message:flags.11?string reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> geo_point:flags.13?InputGeoPoint = Bool;
messages.getBotCallbackAnswer#810a9fec flags:# game:flags.1?true peer:InputPeer msg_id:int data:flags.0?bytes = messages.BotCallbackAnswer; messages.getBotCallbackAnswer#810a9fec flags:# game:flags.1?true peer:InputPeer msg_id:int data:flags.0?bytes = messages.BotCallbackAnswer;
messages.setBotCallbackAnswer#d58f130a flags:# alert:flags.1?true query_id:long message:flags.0?string url:flags.2?string cache_time:int = Bool; messages.setBotCallbackAnswer#d58f130a flags:# alert:flags.1?true query_id:long message:flags.0?string url:flags.2?string cache_time:int = Bool;
messages.getPeerDialogs#2d9776b9 peers:Vector<InputPeer> = messages.PeerDialogs; messages.getPeerDialogs#2d9776b9 peers:Vector<InputPeer> = messages.PeerDialogs;
@ -1098,9 +1100,10 @@ messages.sendScreenshotNotification#c97df020 peer:InputPeer reply_to_msg_id:int
messages.getFavedStickers#21ce0b0e hash:int = messages.FavedStickers; messages.getFavedStickers#21ce0b0e hash:int = messages.FavedStickers;
messages.faveSticker#b9ffc55b id:InputDocument unfave:Bool = Bool; messages.faveSticker#b9ffc55b id:InputDocument unfave:Bool = Bool;
messages.getUnreadMentions#46578472 peer:InputPeer offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; messages.getUnreadMentions#46578472 peer:InputPeer offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages;
messages.getRecentLocations#249431e2 peer:InputPeer limit:int = messages.Messages;
messages.readMentions#f0189d3 peer:InputPeer = messages.AffectedHistory; messages.readMentions#f0189d3 peer:InputPeer = messages.AffectedHistory;
messages.getRecentLocations#249431e2 peer:InputPeer limit:int = messages.Messages;
messages.sendMultiMedia#2095512f flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int multi_media:Vector<InputSingleMedia> = Updates; messages.sendMultiMedia#2095512f flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int multi_media:Vector<InputSingleMedia> = Updates;
messages.uploadEncryptedFile#5057c497 peer:InputEncryptedChat file:InputEncryptedFile = EncryptedFile;
updates.getState#edd4882a = updates.State; updates.getState#edd4882a = updates.State;
updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference; updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference;
@ -1153,7 +1156,7 @@ channels.inviteToChannel#199f3a6c channel:InputChannel users:Vector<InputUser> =
channels.exportInvite#c7560885 channel:InputChannel = ExportedChatInvite; channels.exportInvite#c7560885 channel:InputChannel = ExportedChatInvite;
channels.deleteChannel#c0111fe3 channel:InputChannel = Updates; channels.deleteChannel#c0111fe3 channel:InputChannel = Updates;
channels.toggleInvites#49609307 channel:InputChannel enabled:Bool = Updates; channels.toggleInvites#49609307 channel:InputChannel enabled:Bool = Updates;
channels.exportMessageLink#c846d22d channel:InputChannel id:int = ExportedMessageLink; channels.exportMessageLink#ceb77163 channel:InputChannel id:int grouped:Bool = ExportedMessageLink;
channels.toggleSignatures#1f69b606 channel:InputChannel enabled:Bool = Updates; channels.toggleSignatures#1f69b606 channel:InputChannel enabled:Bool = Updates;
channels.updatePinnedMessage#a72ded52 flags:# silent:flags.0?true channel:InputChannel id:int = Updates; channels.updatePinnedMessage#a72ded52 flags:# silent:flags.0?true channel:InputChannel id:int = Updates;
channels.getAdminedPublicChannels#8d8d82d7 = messages.Chats; channels.getAdminedPublicChannels#8d8d82d7 = messages.Chats;
@ -1193,4 +1196,4 @@ langpack.getStrings#2e1ee318 lang_code:string keys:Vector<string> = Vector<LangP
langpack.getDifference#b2e4d7d from_version:int = LangPackDifference; langpack.getDifference#b2e4d7d from_version:int = LangPackDifference;
langpack.getLanguages#800fd57d = Vector<LangPackLanguage>; langpack.getLanguages#800fd57d = Vector<LangPackLanguage>;
// LAYER 73 // LAYER 74

View File

@ -311,8 +311,10 @@ class TLGenerator:
for ra in repeated_args.values(): for ra in repeated_args.values():
if len(ra) > 1: if len(ra) > 1:
cnd1 = ('self.{}'.format(a.name) for a in ra) cnd1 = ('(self.{0} or self.{0} is not None)'
cnd2 = ('not self.{}'.format(a.name) for a in ra) .format(a.name) for a in ra)
cnd2 = ('(self.{0} is None or self.{0} is False)'
.format(a.name) for a in ra)
builder.writeln( builder.writeln(
"assert ({}) or ({}), '{} parameters must all " "assert ({}) or ({}), '{} parameters must all "
"be False-y (like None) or all me True-y'".format( "be False-y (like None) or all me True-y'".format(
@ -464,9 +466,11 @@ class TLGenerator:
# Vector flags are special since they consist of 3 values, # Vector flags are special since they consist of 3 values,
# so we need an extra join here. Note that empty vector flags # so we need an extra join here. Note that empty vector flags
# should NOT be sent either! # should NOT be sent either!
builder.write("b'' if not {} else b''.join((".format(name)) builder.write("b'' if {0} is None or {0} is False "
"else b''.join((".format(name))
else: else:
builder.write("b'' if not {} else (".format(name)) builder.write("b'' if {0} is None or {0} is False "
"else (".format(name))
if arg.is_vector: if arg.is_vector:
if arg.use_vector_id: if arg.use_vector_id:
@ -495,11 +499,14 @@ class TLGenerator:
# There's a flag indicator, but no flag arguments so it's 0 # There's a flag indicator, but no flag arguments so it's 0
builder.write(r"b'\0\0\0\0'") builder.write(r"b'\0\0\0\0'")
else: else:
builder.write("struct.pack('<I', {})".format( builder.write("struct.pack('<I', ")
' | '.join('({} if {} else 0)'.format( builder.write(
1 << flag.flag_index, 'self.{}'.format(flag.name) ' | '.join('(0 if {0} is None or {0} is False else {1})'
) for flag in args if flag.is_flag) .format('self.{}'.format(flag.name),
)) 1 << flag.flag_index)
for flag in args if flag.is_flag)
)
builder.write(')')
elif 'int' == arg.type: elif 'int' == arg.type:
# struct.pack is around 4 times faster than int.to_bytes # struct.pack is around 4 times faster than int.to_bytes
@ -533,11 +540,7 @@ class TLGenerator:
builder.write('TLObject.serialize_bytes({})'.format(name)) builder.write('TLObject.serialize_bytes({})'.format(name))
elif 'date' == arg.type: # Custom format elif 'date' == arg.type: # Custom format
# 0 if datetime is None else int(datetime.timestamp()) builder.write('TLObject.serialize_datetime({})'.format(name))
builder.write(
r"b'\0\0\0\0' if {0} is None else "
r"struct.pack('<I', int({0}.timestamp()))".format(name)
)
else: else:
# Else it may be a custom type # Else it may be a custom type

View File

@ -18,7 +18,7 @@ class HigherLevelTests(unittest.TestCase):
@staticmethod @staticmethod
def test_cdn_download(): def test_cdn_download():
client = TelegramClient(None, api_id, api_hash) client = TelegramClient(None, api_id, api_hash)
client.session.server_address = '149.154.167.40' client.session.set_dc(0, '149.154.167.40', 80)
assert client.connect() assert client.connect()
try: try: