mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-08-03 11:40:11 +03:00
Merge branch 'master' into asyncio
This commit is contained in:
commit
a1d497a2c0
14
README.rst
14
README.rst
|
@ -53,16 +53,16 @@ if you're new with ``asyncio``.
|
|||
await client.send_file('username', '/home/myself/Pictures/holidays.jpg')
|
||||
|
||||
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])
|
||||
|
||||
|
||||
Next steps
|
||||
----------
|
||||
|
||||
Do you like how Telethon looks? Check the
|
||||
`wiki over GitHub <https://github.com/LonamiWebs/Telethon/wiki>`_ for a
|
||||
more in-depth explanation, with examples, troubleshooting issues, and more
|
||||
useful information. Note that the examples there are written for the threaded
|
||||
version, not the one using asyncio. However, you just need to await every
|
||||
remote call.
|
||||
Do you like how Telethon looks? Check out
|
||||
`Read The Docs <http://telethon.rtfd.io/>`_
|
||||
for a more in-depth explanation, with examples, troubleshooting issues,
|
||||
and more useful information. Note that the examples there are written for
|
||||
the threaded version, not the one using asyncio. However, you just need to
|
||||
await every remote call.
|
||||
|
|
|
@ -90,7 +90,7 @@ class DocsWriter:
|
|||
def end_menu(self):
|
||||
"""Ends an opened menu"""
|
||||
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>')
|
||||
|
||||
def write_title(self, title, level=1):
|
||||
|
|
|
@ -1,33 +1,41 @@
|
|||
.. _accessing-the-full-api:
|
||||
|
||||
==========================
|
||||
======================
|
||||
Accessing the Full API
|
||||
==========================
|
||||
======================
|
||||
|
||||
The ``TelegramClient`` doesn’t offer a method for every single request
|
||||
the Telegram API supports. However, it’s very simple to ``.invoke()``
|
||||
any request. Whenever you need something, don’t forget to `check the
|
||||
|
||||
The ``TelegramClient`` doesn't offer a method for every single request
|
||||
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
|
||||
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
|
||||
(constructors) Telegram returns look like. Every constructor inherits
|
||||
from a common type, and that’s the reason for this distinction.
|
||||
from a common type, and that's the reason for this distinction.
|
||||
|
||||
Say ``client.send_message()`` didn’t exist, we could use the `search`__
|
||||
to look for “message”. There we would find `SendMessageRequest`__,
|
||||
Say ``client.send_message()`` didn't exist, we could use the `search`__
|
||||
to look for "message". There we would find `SendMessageRequest`__,
|
||||
which we can work with.
|
||||
|
||||
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
|
||||
what input parameters it takes. Remember to “Copy import to the
|
||||
clipboard”, or your script won’t be aware of this class! Now we have:
|
||||
what input parameters it takes. Remember to "Copy import to the
|
||||
clipboard", or your script won't be aware of this class! Now we have:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.functions.messages import SendMessageRequest
|
||||
|
||||
If you’re 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
|
||||
|
||||
|
@ -53,20 +61,20 @@ Or we call ``.get_input_entity()``:
|
|||
|
||||
peer = client.get_input_entity('someone')
|
||||
|
||||
When you’re 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
|
||||
``.get_input_entity()`` is more straightforward (and sometimes
|
||||
immediate, if you know the ID of the user for instance). If you also
|
||||
need to have information about the whole user, use ``.get_entity()``
|
||||
instead:
|
||||
``.get_input_entity()`` is more straightforward (and often
|
||||
immediate, if you've seen the user before, know their ID, etc.).
|
||||
If you also need to have information about the whole user, use
|
||||
``.get_entity()`` instead:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
entity = client.get_entity('someone')
|
||||
|
||||
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
|
||||
want to cache its input version so the library doesn’t have to do this
|
||||
its "input" version for you. If you already have the complete user and
|
||||
want to cache its input version so the library doesn't have to do this
|
||||
every time its used, simply call ``.get_input_peer``:
|
||||
|
||||
.. code-block:: python
|
||||
|
@ -83,10 +91,9 @@ request we do:
|
|||
result = client(SendMessageRequest(peer, 'Hello there!'))
|
||||
# __call__ is an alias for client.invoke(request). Both will work
|
||||
|
||||
Message sent! Of course, this is only an example.
|
||||
There are nearly 250 methods available as of layer 73,
|
||||
and you can use every single of them as you wish.
|
||||
Remember to use the right types! To sum up:
|
||||
Message sent! Of course, this is only an example. There are nearly 250
|
||||
methods available as of layer 73, and you can use every single of them
|
||||
as you wish. Remember to use the right types! To sum up:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -97,16 +104,16 @@ Remember to use the right types! To sum up:
|
|||
|
||||
.. note::
|
||||
|
||||
Note that some requests have a "hash" parameter. This is **not** 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).
|
||||
Note that some requests have a "hash" parameter. This is **not**
|
||||
your ``api_hash``! It likely isn't your self-user ``.access_hash`` either.
|
||||
|
||||
For those requests having a "limit" parameter,
|
||||
you can often set it to zero to signify "return as many items as possible".
|
||||
This won't work for all of them though,
|
||||
for instance, in "messages.search" it will actually return 0 items.
|
||||
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, 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
|
||||
|
@ -114,4 +121,4 @@ __ https://lonamiwebs.github.io/Telethon/methods/index.html
|
|||
__ https://lonamiwebs.github.io/Telethon/?q=message
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/messages/send_message.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
|
46
readthedocs/extra/advanced-usage/sessions.rst
Normal file
46
readthedocs/extra/advanced-usage/sessions.rst
Normal 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.
|
|
@ -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!
|
||||
|
|
@ -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 you’ve 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 can’t 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 don’t 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 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)
|
||||
# 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
|
||||
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
|
||||
|
||||
|
||||
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
|
|
@ -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
|
|
@ -1,24 +1,28 @@
|
|||
.. _creating-a-client:
|
||||
|
||||
===================
|
||||
=================
|
||||
Creating a Client
|
||||
===================
|
||||
=================
|
||||
|
||||
|
||||
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.
|
||||
|
||||
3. A *Create new application* window will appear. Fill in your application details.
|
||||
There is no need to enter any *URL*, and only the first two fields (*App title* and *Short name*)
|
||||
can be changed later as far as I'm aware.
|
||||
3. A *Create new application* window will appear. Fill in your application
|
||||
details. There is no need to enter any *URL*, and only the first two
|
||||
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**
|
||||
and Telegram won't let you revoke it. Don't post it anywhere!
|
||||
4. Click on *Create application* at the end. Remember that your
|
||||
**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``.
|
||||
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
|
||||
|
||||
|
@ -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)
|
||||
|
||||
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``
|
||||
|
||||
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``
|
||||
|
||||
|
@ -52,13 +60,25 @@ If you're not authorized, you need to ``.sign_in()``:
|
|||
# If .sign_in raises SessionPasswordNeeded error, call .sign_in(password=...)
|
||||
# You can import both exceptions from telethon.errors.
|
||||
|
||||
``myself`` is your Telegram user.
|
||||
You can view all the information about yourself by doing ``print(myself.stringify())``.
|
||||
You're now ready to use the client as you wish!
|
||||
``myself`` is your Telegram user. You can view all the information about
|
||||
yourself by doing ``print(myself.stringify())``. You're now ready to use
|
||||
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::
|
||||
If you want to use a **proxy**, you have to `install PySocks`__ (via pip or manual)
|
||||
and then set the appropriated parameters:
|
||||
If you want to use a **proxy**, you have to `install PySocks`__
|
||||
(via pip or manual) and then set the appropriated parameters:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -72,5 +92,58 @@ You're now ready to use the client as you wish!
|
|||
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#usage-1%3E
|
||||
__ https://github.com/Anorov/PySocks#usage-1
|
||||
|
|
87
readthedocs/extra/basic/entities.rst
Normal file
87
readthedocs/extra/basic/entities.rst
Normal 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.
|
|
@ -3,13 +3,13 @@
|
|||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
===============
|
||||
Getting Started
|
||||
===============
|
||||
|
||||
=================
|
||||
Getting Started!
|
||||
=================
|
||||
|
||||
Simple Installation
|
||||
*********************
|
||||
*******************
|
||||
|
||||
``pip install telethon``
|
||||
|
||||
|
@ -17,7 +17,7 @@ Simple Installation
|
|||
|
||||
|
||||
Creating a client
|
||||
**************
|
||||
*****************
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -39,8 +39,9 @@ Creating a client
|
|||
**More details**: :ref:`creating-a-client`
|
||||
|
||||
|
||||
Simple Stuff
|
||||
**************
|
||||
Basic Usage
|
||||
***********
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
print(me.stringify())
|
||||
|
@ -49,6 +50,7 @@ Simple Stuff
|
|||
client.send_file('username', '/home/myself/Pictures/holidays.jpg')
|
||||
|
||||
client.download_profile_photo(me)
|
||||
total, messages, senders = client.get_message_history('username')
|
||||
messages = client.get_message_history('username')
|
||||
client.download_media(messages[0])
|
||||
|
||||
**More details**: :ref:`telegram-client`
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
.. _installation:
|
||||
|
||||
=================
|
||||
============
|
||||
Installation
|
||||
=================
|
||||
============
|
||||
|
||||
|
||||
Automatic Installation
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
**********************
|
||||
|
||||
To install Telethon, simply do:
|
||||
|
||||
``pip install telethon``
|
||||
|
||||
If you get something like ``"SyntaxError: invalid syntax"`` or any other error while installing,
|
||||
it's probably because ``pip`` defaults to Python 2, which is not supported. Use ``pip3`` instead.
|
||||
If you get something like ``"SyntaxError: invalid syntax"`` or any other
|
||||
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:
|
||||
|
||||
|
@ -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:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: sh
|
||||
|
||||
# pip install git+https://github.com/LonamiWebs/Telethon.git
|
||||
or
|
||||
|
@ -32,13 +34,15 @@ If you don't have root access, simply pass the ``--user`` flag to the pip comman
|
|||
|
||||
|
||||
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``
|
||||
|
||||
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``
|
||||
|
||||
|
@ -50,22 +54,14 @@ To generate the documentation, ``cd docs`` and then ``python3 generate.py``.
|
|||
|
||||
|
||||
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://pypi.python.org/pypi/pyaes
|
||||
__ https://github.com/sybrenstuvel/python-rsa/
|
||||
__ https://pypi.python.org/pypi/rsa/3.4.2
|
||||
__ https://github.com/LonamiWebs/Telethon/issues/199
|
|
@ -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.
|
|
@ -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 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 (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 you’re not going to work without 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``.
|
||||
|
||||
If you don’t 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.
|
99
readthedocs/extra/basic/telegram-client.rst
Normal file
99
readthedocs/extra/basic/telegram-client.rst
Normal 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:
|
|
@ -14,23 +14,24 @@ The library can run in four distinguishable modes:
|
|||
- With several worker threads that run your update handlers.
|
||||
- A mix of the above.
|
||||
|
||||
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.
|
||||
Since this section is about updates, we'll describe the simplest way to
|
||||
work with them.
|
||||
|
||||
|
||||
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)``
|
||||
|
||||
4 workers should suffice for most cases (this is also the default on `Python Telegram Bot`__).
|
||||
You can set this value to more, or even less if you need.
|
||||
4 workers should suffice for most cases (this is also the default on
|
||||
`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
|
||||
|
||||
|
@ -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!
|
||||
|
||||
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
|
||||
|
||||
|
@ -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!')
|
||||
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
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
*************************
|
||||
|
||||
All the workers do is loop forever and poll updates from a queue that is filled from the ``ReadThread``,
|
||||
responsible for reading every item off the network.
|
||||
If you only need a worker and the ``MainThread`` would be doing no other job,
|
||||
this is the preferred way. You can easily do the same as the workers like so:
|
||||
All the workers do is loop forever and poll updates from a queue that is
|
||||
filled from the ``ReadThread``, responsible for reading every item off
|
||||
the network. If you only need a worker and the ``MainThread`` would be
|
||||
doing no other job, this is the preferred way. You can easily do the same
|
||||
as the workers like so:
|
||||
|
||||
.. 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()
|
||||
|
||||
Note that ``poll`` accepts a ``timeout=`` parameter,
|
||||
and it will return ``None`` if other thread got the update before you could or if the timeout expired,
|
||||
so it's important to check ``if not update``.
|
||||
Note that ``poll`` accepts a ``timeout=`` parameter, and it will return
|
||||
``None`` if other thread got the update before you could or if the timeout
|
||||
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)``
|
||||
|
||||
You **must** set it to ``0`` (or other number), as it defaults to ``None`` and there is a different.
|
||||
``None`` workers means updates won't be processed *at all*,
|
||||
so you must set it to some value (0 or greater) if you want ``client.updates.poll()`` to work.
|
||||
You **must** set it to ``0`` (or other number), as it defaults to ``None``
|
||||
and there is a different. ``None`` workers means updates won't be processed
|
||||
*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``
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
************************************************
|
||||
|
||||
If you have no work to do on the ``MainThread`` and you were planning to have a ``while True: sleep(1)``,
|
||||
don't do that. Instead, don't spawn the secondary ``ReadThread`` at all like so:
|
||||
If you have no work to do on the ``MainThread`` and you were planning to have
|
||||
a ``while True: sleep(1)``, don't do that. Instead, don't spawn the secondary
|
||||
``ReadThread`` at all like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -111,8 +118,8 @@ And then ``.idle()`` from the ``MainThread``:
|
|||
|
||||
``client.idle()``
|
||||
|
||||
You can stop it with :kbd:`Control+C`,
|
||||
and you can configure the signals to be used in a similar fashion to `Python Telegram Bot`__.
|
||||
You can stop it with :kbd:`Control+C`, and you can configure the signals
|
||||
to be used in a similar fashion to `Python Telegram Bot`__.
|
||||
|
||||
As a complete example:
|
||||
|
||||
|
@ -132,4 +139,4 @@ As a complete example:
|
|||
|
||||
__ https://python-telegram-bot.org/
|
||||
__ 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
|
||||
|
|
54
readthedocs/extra/developing/api-status.rst
Normal file
54
readthedocs/extra/developing/api-status.rst
Normal 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
|
22
readthedocs/extra/developing/coding-style.rst
Normal file
22
readthedocs/extra/developing/coding-style.rst
Normal 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``!
|
25
readthedocs/extra/developing/philosophy.rst
Normal file
25
readthedocs/extra/developing/philosophy.rst
Normal 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.
|
43
readthedocs/extra/developing/project-structure.rst
Normal file
43
readthedocs/extra/developing/project-structure.rst
Normal 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.
|
|
@ -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.
|
32
readthedocs/extra/developing/test-servers.rst
Normal file
32
readthedocs/extra/developing/test-servers.rst
Normal 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')
|
|
@ -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!
|
|
@ -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.
|
|
@ -1,13 +1,14 @@
|
|||
======
|
||||
====
|
||||
Bots
|
||||
======
|
||||
====
|
||||
|
||||
|
||||
Talking to Inline Bots
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
**********************
|
||||
|
||||
You can query an inline bot, such as `@VoteBot`__
|
||||
(note, *query*, not *interact* with a voting message), by making use of
|
||||
the `GetInlineBotResultsRequest`__ request:
|
||||
You can query an inline bot, such as `@VoteBot`__ (note, *query*,
|
||||
not *interact* with a voting message), by making use of the
|
||||
`GetInlineBotResultsRequest`__ request:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -32,11 +33,10 @@ And you can select any of their results by using
|
|||
|
||||
|
||||
Talking to Bots with special reply markup
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
*****************************************
|
||||
|
||||
To interact with a message that has a special reply markup, such as
|
||||
`@VoteBot`__ polls, you would use
|
||||
`GetBotCallbackAnswerRequest`__:
|
||||
`@VoteBot`__ polls, you would use `GetBotCallbackAnswerRequest`__:
|
||||
|
||||
.. 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
|
||||
))
|
||||
|
||||
It’s 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
|
||||
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/send_inline_bot_result.html
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/messages/get_bot_callback_answer.html
|
||||
__ https://t.me/vote
|
||||
__ https://t.me/vote
|
205
readthedocs/extra/examples/chats-and-channels.rst
Normal file
205
readthedocs/extra/examples/chats-and-channels.rst
Normal 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
|
|
@ -1,20 +1,18 @@
|
|||
=========================
|
||||
=====================
|
||||
Working with messages
|
||||
=========================
|
||||
|
||||
.. note::
|
||||
Make sure you have gone through :ref:`prelude` already!
|
||||
=====================
|
||||
|
||||
|
||||
Forwarding messages
|
||||
*******************
|
||||
|
||||
Note that ForwardMessageRequest_ (note it's Message, singular) will *not* work if channels are involved.
|
||||
This is because channel (and megagroups) IDs are not unique, so you also need to know who the sender is
|
||||
(a parameter this request doesn't have).
|
||||
Note that ForwardMessageRequest_ (note it's Message, singular) will *not*
|
||||
work if channels are involved. This is because channel (and megagroups) IDs
|
||||
are not unique, so you also need to know who the sender is (a parameter this
|
||||
request doesn't have).
|
||||
|
||||
Either way, you are encouraged to use ForwardMessagesRequest_ (note it's Message*s*, plural) *always*,
|
||||
since it is more powerful, as follows:
|
||||
Either way, you are encouraged to use ForwardMessagesRequest_ (note it's
|
||||
Message*s*, plural) *always*, since it is more powerful, as follows:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -31,14 +29,16 @@ since it is more powerful, as follows:
|
|||
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.
|
||||
You can obviously just wrap a single message on the list too, if that's all you have.
|
||||
The named arguments are there for clarity, although they're not needed because
|
||||
they appear in order. You can obviously just wrap a single message on the list
|
||||
too, if that's all you have.
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
@ -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
|
||||
))
|
||||
|
||||
It's important to note that the optional parameter ``from_id`` has been left 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,
|
||||
It's important to note that the optional parameter ``from_id`` has been left
|
||||
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.
|
||||
|
||||
If one were to set ``from_id=InputUserEmpty()``, it would filter messages from "empty" senders,
|
||||
which would likely match no users.
|
||||
If one were to set ``from_id=InputUserEmpty()``, it would filter messages
|
||||
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,
|
||||
and as the error says, you can't do that. Leave it set to ``None`` and it should work.
|
||||
If you get a ``ChatAdminRequiredError`` on a channel, it's probably because
|
||||
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``,
|
||||
or you'll likely run into errors like ``UserIdInvalidError``.
|
||||
As with every method, make sure you use the right ID/hash combination for
|
||||
your ``InputUser`` or ``InputChat``, or you'll likely run into errors like
|
||||
``UserIdInvalidError``.
|
||||
|
||||
|
||||
Sending stickers
|
||||
*****************
|
||||
****************
|
||||
|
||||
Stickers are nothing else than ``files``, and when you successfully retrieve the stickers for a certain sticker set,
|
||||
all you will have are ``handles`` to these files. Remember, the files Telegram holds on their servers can be 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:
|
||||
Stickers are nothing else than ``files``, and when you successfully retrieve
|
||||
the stickers for a certain sticker set, all you will have are ``handles`` to
|
||||
these files. Remember, the files Telegram holds on their servers can be
|
||||
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
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
=========================================
|
||||
========================================
|
||||
Deleted, Limited or Deactivated Accounts
|
||||
=========================================
|
||||
========================================
|
||||
|
||||
If you're from Iran or Russian, we have bad news for you.
|
||||
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://github.com/LonamiWebs/Telethon/issues/297
|
||||
__ https://github.com/LonamiWebs/Telethon/issues/297
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
================
|
||||
Enable Logging
|
||||
Enabling Logging
|
||||
================
|
||||
|
||||
Telethon makes use of the `logging`__ module, and you can enable it as follows:
|
||||
|
||||
.. code-block:: python
|
||||
.. code:: python
|
||||
|
||||
import logging
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
import logging
|
||||
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
|
||||
|
||||
|
@ -21,4 +24,17 @@ You can also use it in your own project very easily:
|
|||
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
|
||||
|
|
|
@ -3,17 +3,17 @@ RPC Errors
|
|||
==========
|
||||
|
||||
RPC stands for Remote Procedure Call, and when Telethon raises an
|
||||
``RPCError``, it’s 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
|
||||
something went wrong on Telegram’s 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
|
||||
wait ``.seconds``.
|
||||
- ``FloodWaitError`` (420), the same request was repeated many times.
|
||||
Must wait ``.seconds`` (you can access this parameter).
|
||||
- ``SessionPasswordNeededError``, if you have setup two-steps
|
||||
verification on Telegram.
|
||||
- ``CdnFileTamperedError``, if the media you were trying to download
|
||||
from a CDN has been altered.
|
||||
- ``ChatAdminRequiredError``, you don’t 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
|
||||
searching messages.
|
||||
|
||||
|
@ -22,6 +22,6 @@ The generic classes for different error codes are: \* ``InvalidDCError``
|
|||
``BadRequestError`` (400), the request contained errors. \*
|
||||
``UnauthorizedError`` (401), the user is not authorized yet. \*
|
||||
``ForbiddenError`` (403), privacy violation error. \* ``NotFoundError``
|
||||
(404), make sure you’re 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``.
|
||||
|
|
62
readthedocs/extra/wall-of-shame.rst
Normal file
62
readthedocs/extra/wall-of-shame.rst
Normal 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
|
|
@ -3,11 +3,14 @@
|
|||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
====================================
|
||||
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:
|
||||
|
@ -19,10 +22,9 @@ Pure Python 3 Telegram client library. Official Site `here <https://lonamiwebs.g
|
|||
extra/basic/getting-started
|
||||
extra/basic/installation
|
||||
extra/basic/creating-a-client
|
||||
extra/basic/sessions
|
||||
extra/basic/sending-requests
|
||||
extra/basic/telegram-client
|
||||
extra/basic/entities
|
||||
extra/basic/working-with-updates
|
||||
extra/basic/accessing-the-full-api
|
||||
|
||||
|
||||
.. _Advanced-usage:
|
||||
|
@ -31,11 +33,19 @@ Pure Python 3 Telegram client library. Official Site `here <https://lonamiwebs.g
|
|||
:maxdepth: 2
|
||||
:caption: Advanced Usage
|
||||
|
||||
extra/advanced
|
||||
extra/advanced-usage/signing-in
|
||||
extra/advanced-usage/working-with-messages
|
||||
extra/advanced-usage/users-and-chats
|
||||
extra/advanced-usage/bots
|
||||
extra/advanced-usage/accessing-the-full-api
|
||||
extra/advanced-usage/sessions
|
||||
|
||||
|
||||
.. _Examples:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Examples
|
||||
|
||||
extra/examples/working-with-messages
|
||||
extra/examples/chats-and-channels
|
||||
extra/examples/bots
|
||||
|
||||
|
||||
.. _Troubleshooting:
|
||||
|
@ -49,6 +59,30 @@ Pure Python 3 Telegram client library. Official Site `here <https://lonamiwebs.g
|
|||
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::
|
||||
:caption: Telethon modules
|
||||
|
@ -56,7 +90,6 @@ Pure Python 3 Telegram client library. Official Site `here <https://lonamiwebs.g
|
|||
telethon
|
||||
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
|
|
10
setup.py
10
setup.py
|
@ -71,6 +71,16 @@ def main():
|
|||
print('Done.')
|
||||
|
||||
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
|
||||
# Place it here since noone should be running ./setup.py pypi anyway
|
||||
from subprocess import run
|
||||
|
|
|
@ -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_ctr import AESModeCTR
|
||||
from .auth_key import AuthKey
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
"""
|
||||
AES IGE implementation in Python. This module may use libssl if available.
|
||||
"""
|
||||
import os
|
||||
import pyaes
|
||||
from . import libssl
|
||||
|
@ -9,10 +12,15 @@ if libssl.AES is not None:
|
|||
else:
|
||||
# Fallback to a pure Python implementation
|
||||
class AES:
|
||||
"""
|
||||
Class that servers as an interface to encrypt and decrypt
|
||||
text through the AES IGE mode.
|
||||
"""
|
||||
@staticmethod
|
||||
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]
|
||||
iv2 = iv[len(iv) // 2:]
|
||||
|
@ -42,8 +50,9 @@ else:
|
|||
|
||||
@staticmethod
|
||||
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
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
"""
|
||||
This module holds the AESModeCTR wrapper class.
|
||||
"""
|
||||
import pyaes
|
||||
|
||||
|
||||
|
@ -6,6 +9,12 @@ class AESModeCTR:
|
|||
# TODO Maybe make a pull request to pyaes to support iv on CTR
|
||||
|
||||
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
|
||||
assert isinstance(key, bytes)
|
||||
self._aes = pyaes.AESModeOfOperationCTR(key)
|
||||
|
@ -15,7 +24,19 @@ class AESModeCTR:
|
|||
self._aes._counter._counter = list(iv)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
"""
|
||||
This module holds the AuthKey class.
|
||||
"""
|
||||
import struct
|
||||
from hashlib import sha1
|
||||
|
||||
|
@ -6,7 +9,16 @@ from ..extensions import BinaryReader
|
|||
|
||||
|
||||
class AuthKey:
|
||||
"""
|
||||
Represents an authorization key, used to encrypt and decrypt
|
||||
messages sent to Telegram's data centers.
|
||||
"""
|
||||
def __init__(self, data):
|
||||
"""
|
||||
Initializes a new authorization key.
|
||||
|
||||
:param data: the data in bytes that represent this auth key.
|
||||
"""
|
||||
self.key = data
|
||||
|
||||
with BinaryReader(sha1(self.key).digest()) as reader:
|
||||
|
@ -15,8 +27,12 @@ class AuthKey:
|
|||
self.key_id = reader.read_long(signed=False)
|
||||
|
||||
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)
|
||||
data = new_nonce + struct.pack('<BQ', number, self.aux_hash)
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
"""
|
||||
This module holds the CdnDecrypter utility class.
|
||||
"""
|
||||
from hashlib import sha256
|
||||
|
||||
from ..tl import Session
|
||||
from ..tl.functions.upload import GetCdnFileRequest, ReuploadCdnFileRequest
|
||||
from ..tl.types.upload import CdnFileReuploadNeeded, CdnFile
|
||||
from ..crypto import AESModeCTR
|
||||
|
@ -8,11 +10,20 @@ from ..errors import CdnFileTamperedError
|
|||
|
||||
|
||||
class CdnDecrypter:
|
||||
"""Used when downloading a file results in a 'FileCdnRedirect' to
|
||||
both prepare the redirect, decrypt the file as it downloads, and
|
||||
ensure the file hasn't been tampered. https://core.telegram.org/cdn
|
||||
"""
|
||||
Used when downloading a file results in a 'FileCdnRedirect' to
|
||||
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):
|
||||
"""
|
||||
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.file_token = file_token
|
||||
self.cdn_aes = cdn_aes
|
||||
|
@ -20,10 +31,13 @@ class CdnDecrypter:
|
|||
|
||||
@staticmethod
|
||||
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.
|
||||
'cdn_client' should be an already-connected TelegramBareClient
|
||||
with the auth key already created.
|
||||
"""
|
||||
Prepares a new CDN decrypter.
|
||||
|
||||
: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(
|
||||
key=cdn_redirect.encryption_key,
|
||||
|
@ -60,8 +74,11 @@ class CdnDecrypter:
|
|||
return decrypter, cdn_file
|
||||
|
||||
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:
|
||||
cdn_hash = self.cdn_file_hashes.pop(0)
|
||||
|
@ -77,6 +94,12 @@ class CdnDecrypter:
|
|||
|
||||
@staticmethod
|
||||
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:
|
||||
raise CdnFileTamperedError()
|
||||
|
|
|
@ -1,9 +1,21 @@
|
|||
"""
|
||||
This module holds a fast Factorization class.
|
||||
"""
|
||||
from random import randint
|
||||
|
||||
|
||||
class Factorization:
|
||||
"""
|
||||
Simple module to factorize large numbers really quickly.
|
||||
"""
|
||||
@classmethod
|
||||
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:
|
||||
return 2, pq // 2
|
||||
|
||||
|
@ -39,6 +51,13 @@ class Factorization:
|
|||
|
||||
@staticmethod
|
||||
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:
|
||||
a, b = b, a % b
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
"""
|
||||
This module holds an AES IGE class, if libssl is available on the system.
|
||||
"""
|
||||
import os
|
||||
import ctypes
|
||||
from ctypes.util import find_library
|
||||
|
@ -35,14 +38,23 @@ else:
|
|||
AES_DECRYPT = ctypes.c_int(0)
|
||||
|
||||
class AES_KEY(ctypes.Structure):
|
||||
"""Helper class representing an AES key"""
|
||||
_fields_ = [
|
||||
('rd_key', ctypes.c_uint32 * (4*(AES_MAXNR + 1))),
|
||||
('rounds', ctypes.c_uint),
|
||||
]
|
||||
|
||||
class AES:
|
||||
"""
|
||||
Class that servers as an interface to encrypt and decrypt
|
||||
text through the AES IGE mode, using the system's libssl.
|
||||
"""
|
||||
@staticmethod
|
||||
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()
|
||||
ckey = (ctypes.c_ubyte * len(key))(*key)
|
||||
cklen = ctypes.c_int(len(key)*8)
|
||||
|
@ -65,6 +77,10 @@ else:
|
|||
|
||||
@staticmethod
|
||||
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
|
||||
if len(plain_text) % 16 != 0:
|
||||
padding_count = 16 - len(plain_text) % 16
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
"""
|
||||
This module holds several utilities regarding RSA and server fingerprints.
|
||||
"""
|
||||
import os
|
||||
import struct
|
||||
from hashlib import sha1
|
||||
|
@ -32,8 +35,11 @@ def get_byte_array(integer):
|
|||
|
||||
|
||||
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))
|
||||
e = TLObject.serialize_bytes(get_byte_array(key.e))
|
||||
|
@ -49,8 +55,14 @@ def add_key(pub):
|
|||
|
||||
|
||||
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
|
||||
key = _server_keys.get(fingerprint, None)
|
||||
|
|
|
@ -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 re
|
||||
from threading import Thread
|
||||
|
||||
from .common import (
|
||||
ReadCancelledError, InvalidParameterError, TypeNotFoundError,
|
||||
InvalidChecksumError, BrokenAuthKeyError, SecurityError,
|
||||
CdnFileTamperedError
|
||||
ReadCancelledError, TypeNotFoundError, InvalidChecksumError,
|
||||
BrokenAuthKeyError, SecurityError, CdnFileTamperedError
|
||||
)
|
||||
|
||||
# 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):
|
||||
"""
|
||||
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:
|
||||
# Ensure it's signed
|
||||
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):
|
||||
"""
|
||||
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:
|
||||
Thread(
|
||||
target=report_error,
|
||||
|
|
|
@ -2,20 +2,16 @@
|
|||
|
||||
|
||||
class ReadCancelledError(Exception):
|
||||
"""Occurs when a read operation was cancelled"""
|
||||
"""Occurs when a read operation was cancelled."""
|
||||
def __init__(self):
|
||||
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):
|
||||
"""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):
|
||||
super().__init__(
|
||||
self, 'Could not find a matching Constructor ID for the TLObject '
|
||||
|
@ -27,6 +23,10 @@ class TypeNotFoundError(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):
|
||||
super().__init__(
|
||||
self,
|
||||
|
@ -39,6 +39,9 @@ class InvalidChecksumError(Exception):
|
|||
|
||||
|
||||
class BrokenAuthKeyError(Exception):
|
||||
"""
|
||||
Occurs when the authorization key for a data center is not valid.
|
||||
"""
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
self,
|
||||
|
@ -47,6 +50,9 @@ class BrokenAuthKeyError(Exception):
|
|||
|
||||
|
||||
class SecurityError(Exception):
|
||||
"""
|
||||
Generic security error, mostly used when generating a new AuthKey.
|
||||
"""
|
||||
def __init__(self, *args):
|
||||
if not args:
|
||||
args = ['A security check failed.']
|
||||
|
@ -54,6 +60,10 @@ class SecurityError(Exception):
|
|||
|
||||
|
||||
class CdnFileTamperedError(SecurityError):
|
||||
"""
|
||||
Occurs when there's a hash mismatch between the decrypted CDN file
|
||||
and its expected hash.
|
||||
"""
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
'The CDN file has been altered and its download cancelled.'
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
class RPCError(Exception):
|
||||
"""Base class for all Remote Procedure Call errors."""
|
||||
code = None
|
||||
message = None
|
||||
|
||||
|
||||
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
|
||||
message = 'ERROR_SEE_OTHER'
|
||||
|
@ -13,9 +14,9 @@ class InvalidDCError(RPCError):
|
|||
|
||||
class BadRequestError(RPCError):
|
||||
"""
|
||||
The query contains errors. In the event that a request was created
|
||||
using a form and contains user generated data, the user should be
|
||||
notified that the data must be corrected before the query is repeated.
|
||||
The query contains errors. In the event that a request was created
|
||||
using a form and contains user generated data, the user should be
|
||||
notified that the data must be corrected before the query is repeated.
|
||||
"""
|
||||
code = 400
|
||||
message = 'BAD_REQUEST'
|
||||
|
@ -23,8 +24,8 @@ class BadRequestError(RPCError):
|
|||
|
||||
class UnauthorizedError(RPCError):
|
||||
"""
|
||||
There was an unauthorized attempt to use functionality available only
|
||||
to authorized users.
|
||||
There was an unauthorized attempt to use functionality available only
|
||||
to authorized users.
|
||||
"""
|
||||
code = 401
|
||||
message = 'UNAUTHORIZED'
|
||||
|
@ -32,8 +33,8 @@ class UnauthorizedError(RPCError):
|
|||
|
||||
class ForbiddenError(RPCError):
|
||||
"""
|
||||
Privacy violation. For example, an attempt to write a message to
|
||||
someone who has blacklisted the current user.
|
||||
Privacy violation. For example, an attempt to write a message to
|
||||
someone who has blacklisted the current user.
|
||||
"""
|
||||
code = 403
|
||||
message = 'FORBIDDEN'
|
||||
|
@ -45,7 +46,7 @@ class ForbiddenError(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
|
||||
message = 'NOT_FOUND'
|
||||
|
@ -57,10 +58,10 @@ class NotFoundError(RPCError):
|
|||
|
||||
class FloodError(RPCError):
|
||||
"""
|
||||
The maximum allowed number of attempts to invoke the given method
|
||||
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
|
||||
phone number.
|
||||
The maximum allowed number of attempts to invoke the given method
|
||||
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
|
||||
phone number.
|
||||
"""
|
||||
code = 420
|
||||
message = 'FLOOD'
|
||||
|
@ -68,9 +69,9 @@ class FloodError(RPCError):
|
|||
|
||||
class ServerError(RPCError):
|
||||
"""
|
||||
An internal server error occurred while a request was being processed
|
||||
for example, there was a disruption while accessing a database or file
|
||||
storage.
|
||||
An internal server error occurred while a request was being processed
|
||||
for example, there was a disruption while accessing a database or file
|
||||
storage.
|
||||
"""
|
||||
code = 500
|
||||
message = 'INTERNAL'
|
||||
|
@ -81,38 +82,42 @@ class ServerError(RPCError):
|
|||
|
||||
|
||||
class BadMessageError(Exception):
|
||||
"""Occurs when handling a bad_message_notification"""
|
||||
"""Occurs when handling a bad_message_notification."""
|
||||
ErrorMessages = {
|
||||
16:
|
||||
'msg_id too low (most likely, client time is wrong it would be worthwhile to '
|
||||
'synchronize it using msg_id notifications and re-send the original message '
|
||||
'with the "correct" msg_id or wrap it in a container with a new msg_id if the '
|
||||
'original message had waited too long on the client to be transmitted).',
|
||||
'msg_id too low (most likely, client time is wrong it would be '
|
||||
'worthwhile to synchronize it using msg_id notifications and re-send '
|
||||
'the original message with the "correct" msg_id or wrap it in a '
|
||||
'container with a new msg_id if the original message had waited too '
|
||||
'long on the client to be transmitted).',
|
||||
17:
|
||||
'msg_id too high (similar to the previous case, the client time has to be '
|
||||
'synchronized, and the message re-sent with the correct msg_id).',
|
||||
'msg_id too high (similar to the previous case, the client time has '
|
||||
'to be synchronized, and the message re-sent with the correct msg_id).',
|
||||
18:
|
||||
'Incorrect two lower order msg_id bits (the server expects client message msg_id '
|
||||
'to be divisible by 4).',
|
||||
'Incorrect two lower order msg_id bits (the server expects client '
|
||||
'message msg_id to be divisible by 4).',
|
||||
19:
|
||||
'Container msg_id is the same as msg_id of a previously received message '
|
||||
'(this must never happen).',
|
||||
'Container msg_id is the same as msg_id of a previously received '
|
||||
'message (this must never happen).',
|
||||
20:
|
||||
'Message too old, and it cannot be verified whether the server has received a '
|
||||
'message with this msg_id or not.',
|
||||
'Message too old, and it cannot be verified whether the server has '
|
||||
'received a message with this msg_id or not.',
|
||||
32:
|
||||
'msg_seqno too low (the server has already received a message with a lower '
|
||||
'msg_id but with either a higher or an equal and odd seqno).',
|
||||
'msg_seqno too low (the server has already received a message with a '
|
||||
'lower msg_id but with either a higher or an equal and odd seqno).',
|
||||
33:
|
||||
'msg_seqno too high (similarly, there is a message with a higher msg_id but with '
|
||||
'either a lower or an equal and odd seqno).',
|
||||
'msg_seqno too high (similarly, there is a message with a higher '
|
||||
'msg_id but with either a lower or an equal and odd seqno).',
|
||||
34:
|
||||
'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:
|
||||
'Incorrect server salt (in this case, the bad_server_salt response is received with '
|
||||
'the correct salt, and the message is to be re-sent with it).',
|
||||
64: 'Invalid container.'
|
||||
'Incorrect server salt (in this case, the bad_server_salt response '
|
||||
'is received with the correct salt, and the message is to be re-sent '
|
||||
'with it).',
|
||||
64:
|
||||
'Invalid container.'
|
||||
}
|
||||
|
||||
def __init__(self, code):
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
"""
|
||||
This module contains the BinaryReader utility class.
|
||||
"""
|
||||
import os
|
||||
from datetime import datetime
|
||||
from io import BufferedReader, BytesIO
|
||||
from struct import unpack
|
||||
|
||||
from ..errors import InvalidParameterError, TypeNotFoundError
|
||||
from ..errors import TypeNotFoundError
|
||||
from ..tl.all_tlobjects import tlobjects
|
||||
|
||||
|
||||
|
@ -19,8 +22,7 @@ class BinaryReader:
|
|||
elif stream:
|
||||
self.stream = stream
|
||||
else:
|
||||
raise InvalidParameterError(
|
||||
'Either bytes or a stream must be provided')
|
||||
raise ValueError('Either bytes or a stream must be provided')
|
||||
|
||||
self.reader = BufferedReader(self.stream)
|
||||
self._last = None # Should come in handy to spot -404 errors
|
||||
|
@ -30,32 +32,32 @@ class BinaryReader:
|
|||
# "All numbers are written as little endian."
|
||||
# https://core.telegram.org/mtproto
|
||||
def read_byte(self):
|
||||
"""Reads a single byte value"""
|
||||
"""Reads a single byte value."""
|
||||
return self.read(1)[0]
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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]
|
||||
|
||||
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]
|
||||
|
||||
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(
|
||||
self.read(bits // 8), byteorder='little', signed=signed)
|
||||
|
||||
def read(self, length):
|
||||
"""Read the given amount of bytes"""
|
||||
"""Read the given amount of bytes."""
|
||||
result = self.reader.read(length)
|
||||
if len(result) != length:
|
||||
raise BufferError(
|
||||
|
@ -67,7 +69,7 @@ class BinaryReader:
|
|||
return result
|
||||
|
||||
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()
|
||||
|
||||
# endregion
|
||||
|
@ -75,8 +77,9 @@ class BinaryReader:
|
|||
# region Telegram custom reading
|
||||
|
||||
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()
|
||||
if first_byte == 254:
|
||||
|
@ -95,28 +98,28 @@ class BinaryReader:
|
|||
return data
|
||||
|
||||
def tgread_string(self):
|
||||
"""Reads a Telegram-encoded string"""
|
||||
"""Reads a Telegram-encoded string."""
|
||||
return str(self.tgread_bytes(), encoding='utf-8', errors='replace')
|
||||
|
||||
def tgread_bool(self):
|
||||
"""Reads a Telegram boolean value"""
|
||||
"""Reads a Telegram boolean value."""
|
||||
value = self.read_int(signed=False)
|
||||
if value == 0x997275b5: # boolTrue
|
||||
return True
|
||||
elif value == 0xbc799737: # boolFalse
|
||||
return False
|
||||
else:
|
||||
raise ValueError('Invalid boolean code {}'.format(hex(value)))
|
||||
raise RuntimeError('Invalid boolean code {}'.format(hex(value)))
|
||||
|
||||
def tgread_date(self):
|
||||
"""Reads and converts Unix time (used by Telegram)
|
||||
into a Python datetime object
|
||||
into a Python datetime object.
|
||||
"""
|
||||
value = self.read_int()
|
||||
return None if value == 0 else datetime.utcfromtimestamp(value)
|
||||
|
||||
def tgread_object(self):
|
||||
"""Reads a Telegram object"""
|
||||
"""Reads a Telegram object."""
|
||||
constructor_id = self.read_int(signed=False)
|
||||
clazz = tlobjects.get(constructor_id, None)
|
||||
if clazz is None:
|
||||
|
@ -135,9 +138,9 @@ class BinaryReader:
|
|||
return clazz.from_reader(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):
|
||||
raise ValueError('Invalid constructor code, vector was expected')
|
||||
raise RuntimeError('Invalid constructor code, vector was expected')
|
||||
|
||||
count = self.read_int()
|
||||
return [self.tgread_object() for _ in range(count)]
|
||||
|
@ -145,21 +148,23 @@ class BinaryReader:
|
|||
# endregion
|
||||
|
||||
def close(self):
|
||||
"""Closes the reader, freeing the BytesIO stream."""
|
||||
self.reader.close()
|
||||
|
||||
# region Position related
|
||||
|
||||
def tell_position(self):
|
||||
"""Tells the current position on the stream"""
|
||||
"""Tells the current position on the stream."""
|
||||
return self.reader.tell()
|
||||
|
||||
def set_position(self, position):
|
||||
"""Sets the current position on the stream"""
|
||||
"""Sets the current position on the stream."""
|
||||
self.reader.seek(position)
|
||||
|
||||
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)
|
||||
|
||||
|
|
|
@ -33,12 +33,13 @@ ENC = 'utf-16le'
|
|||
|
||||
def parse(message, delimiters=None, url_re=None):
|
||||
"""
|
||||
Parses the given message and returns the stripped message and a list
|
||||
of MessageEntity* using the specified delimiters dictionary (or default
|
||||
if None). The dictionary should be a mapping {delimiter: entity class}.
|
||||
Parses the given markdown message and returns its stripped representation
|
||||
plus a list of the MessageEntity's that were found.
|
||||
|
||||
The url_re(gex) must contain two matching groups: the text to be
|
||||
clickable and the URL itself, and be utf-16le encoded.
|
||||
:param message: the message with markdown-like syntax to be parsed.
|
||||
: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:
|
||||
url_re = DEFAULT_URL_RE
|
||||
|
@ -183,8 +184,13 @@ def unparse(text, entities, delimiters=None, url_fmt=None):
|
|||
|
||||
|
||||
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):
|
||||
entity = (entity,)
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
"""
|
||||
This module holds a rough implementation of the C# TCP client.
|
||||
"""
|
||||
# Python rough implementation of a C# TCP client
|
||||
import asyncio
|
||||
import errno
|
||||
|
@ -13,7 +16,14 @@ CONN_RESET_ERRNOS = {
|
|||
|
||||
|
||||
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):
|
||||
"""
|
||||
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._socket = None
|
||||
self._loop = loop if loop else asyncio.get_event_loop()
|
||||
|
@ -23,7 +33,7 @@ class TcpClient:
|
|||
elif isinstance(timeout, (int, float)):
|
||||
self.timeout = float(timeout)
|
||||
else:
|
||||
raise ValueError('Invalid timeout type', type(timeout))
|
||||
raise TypeError('Invalid timeout type: {}'.format(type(timeout)))
|
||||
|
||||
def _recreate_socket(self, mode):
|
||||
if self.proxy is None:
|
||||
|
@ -39,8 +49,11 @@ class TcpClient:
|
|||
self._socket.setblocking(False)
|
||||
|
||||
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
|
||||
# The address needs to be surrounded by [] as discussed on PR#425
|
||||
|
@ -78,12 +91,13 @@ class TcpClient:
|
|||
raise
|
||||
|
||||
def _get_connected(self):
|
||||
"""Determines whether the client is connected or not."""
|
||||
return self._socket is not None and self._socket.fileno() >= 0
|
||||
|
||||
connected = property(fget=_get_connected)
|
||||
|
||||
def close(self):
|
||||
"""Closes the connection"""
|
||||
"""Closes the connection."""
|
||||
try:
|
||||
if self._socket is not None:
|
||||
self._socket.shutdown(socket.SHUT_RDWR)
|
||||
|
@ -94,7 +108,11 @@ class TcpClient:
|
|||
self._socket = None
|
||||
|
||||
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:
|
||||
self._raise_connection_reset()
|
||||
|
||||
|
@ -106,7 +124,7 @@ class TcpClient:
|
|||
)
|
||||
except asyncio.TimeoutError as e:
|
||||
raise TimeoutError() from e
|
||||
except BrokenPipeError:
|
||||
except ConnectionError:
|
||||
self._raise_connection_reset()
|
||||
except OSError as e:
|
||||
if e.errno in CONN_RESET_ERRNOS:
|
||||
|
@ -115,8 +133,11 @@ class TcpClient:
|
|||
raise
|
||||
|
||||
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:
|
||||
|
@ -132,6 +153,8 @@ class TcpClient:
|
|||
)
|
||||
except asyncio.TimeoutError as e:
|
||||
raise TimeoutError() from e
|
||||
except ConnectionError:
|
||||
self._raise_connection_reset()
|
||||
except OSError as e:
|
||||
if e.errno in CONN_RESET_ERRNOS:
|
||||
self._raise_connection_reset()
|
||||
|
@ -149,6 +172,7 @@ class TcpClient:
|
|||
return buffer.raw.getvalue()
|
||||
|
||||
def _raise_connection_reset(self):
|
||||
"""Disconnects the client and raises ConnectionResetError."""
|
||||
self.close() # Connection reset -> flag as socket closed
|
||||
raise ConnectionResetError('The server has closed the connection.')
|
||||
|
||||
|
|
|
@ -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 .authenticator import do_authentication
|
||||
from .mtproto_sender import MtProtoSender
|
||||
|
|
|
@ -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 time
|
||||
from hashlib import sha1
|
||||
|
@ -18,6 +22,14 @@ from ..tl.functions import (
|
|||
|
||||
|
||||
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:
|
||||
retries = 1
|
||||
|
||||
|
@ -32,9 +44,11 @@ async def do_authentication(connection, retries=5):
|
|||
|
||||
|
||||
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
|
||||
time offset.
|
||||
"""
|
||||
Executes the authentication process with the Telegram servers.
|
||||
|
||||
:param connection: the connection to be used (must be connected).
|
||||
:return: returns a (authorization key, time offset) tuple.
|
||||
"""
|
||||
sender = MtProtoPlainSender(connection)
|
||||
|
||||
|
@ -195,8 +209,12 @@ async def _do_authentication(connection):
|
|||
|
||||
|
||||
def get_int(byte_array, signed=True):
|
||||
"""Gets the specified integer from its byte array.
|
||||
This should be used by the authenticator,
|
||||
who requires the data to be in big endian
|
||||
"""
|
||||
Gets the specified integer from its byte array.
|
||||
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)
|
||||
|
|
|
@ -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 os
|
||||
import struct
|
||||
|
@ -34,16 +38,24 @@ class ConnectionMode(Enum):
|
|||
|
||||
|
||||
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
|
||||
will be packed accordingly, whereas '.write()' and '.read()'
|
||||
work on plain bytes, with no further additions.
|
||||
Note that '.send()' and '.recv()' refer to messages, which
|
||||
will be packed accordingly, whereas '.write()' and '.read()'
|
||||
work on plain bytes, with no further additions.
|
||||
"""
|
||||
|
||||
def __init__(self, mode=ConnectionMode.TCP_FULL,
|
||||
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._send_counter = 0
|
||||
self._aes_encrypt, self._aes_decrypt = None, None
|
||||
|
@ -74,6 +86,12 @@ class Connection:
|
|||
setattr(self, 'read', self._read_plain)
|
||||
|
||||
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:
|
||||
await self.conn.connect(ip, port)
|
||||
except OSError as e:
|
||||
|
@ -91,9 +109,13 @@ class Connection:
|
|||
await self._setup_obfuscation()
|
||||
|
||||
def get_timeout(self):
|
||||
"""Returns the timeout used by the connection."""
|
||||
return self.conn.timeout
|
||||
|
||||
async def _setup_obfuscation(self):
|
||||
"""
|
||||
Sets up the obfuscated protocol.
|
||||
"""
|
||||
# Obfuscated messages secrets cannot start with any of these
|
||||
keywords = (b'PVrG', b'GET ', b'POST', b'\xee' * 4)
|
||||
while True:
|
||||
|
@ -121,13 +143,19 @@ class Connection:
|
|||
await self.conn.write(bytes(random))
|
||||
|
||||
def is_connected(self):
|
||||
"""
|
||||
Determines whether the connection is alive or not.
|
||||
|
||||
:return: true if it's connected.
|
||||
"""
|
||||
return self.conn.connected
|
||||
|
||||
def close(self):
|
||||
"""Closes the connection."""
|
||||
self.conn.close()
|
||||
|
||||
def clone(self):
|
||||
"""Creates a copy of this Connection"""
|
||||
"""Creates a copy of this Connection."""
|
||||
return Connection(
|
||||
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))
|
||||
|
||||
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
|
||||
# potentially await on another self.read(n). Is this guaranteed
|
||||
# by asyncio?
|
||||
|
@ -156,9 +193,21 @@ class Connection:
|
|||
return body
|
||||
|
||||
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])
|
||||
|
||||
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]
|
||||
if length >= 127:
|
||||
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))
|
||||
|
||||
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
|
||||
# total length, sequence number, packet and checksum (CRC32)
|
||||
length = len(message) + 12
|
||||
|
@ -184,9 +239,21 @@ class Connection:
|
|||
await self.write(data + crc)
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
if length < 127:
|
||||
length = struct.pack('B', length)
|
||||
|
@ -203,9 +270,21 @@ class Connection:
|
|||
raise ValueError('Invalid connection mode specified: ' + str(self._mode))
|
||||
|
||||
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)
|
||||
|
||||
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))
|
||||
|
||||
# endregion
|
||||
|
@ -216,9 +295,20 @@ class Connection:
|
|||
raise ValueError('Invalid connection mode specified: ' + str(self._mode))
|
||||
|
||||
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)
|
||||
|
||||
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))
|
||||
|
||||
# endregion
|
||||
|
|
|
@ -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 time
|
||||
|
||||
|
@ -6,32 +10,47 @@ from ..extensions import BinaryReader
|
|||
|
||||
|
||||
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):
|
||||
"""
|
||||
Initializes the MTProto plain sender.
|
||||
|
||||
:param connection: the Connection to be used.
|
||||
"""
|
||||
self._sequence = 0
|
||||
self._time_offset = 0
|
||||
self._last_msg_id = 0
|
||||
self._connection = connection
|
||||
|
||||
async def connect(self):
|
||||
"""Connects to Telegram's servers."""
|
||||
await self._connection.connect()
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnects from Telegram's servers."""
|
||||
self._connection.close()
|
||||
|
||||
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(
|
||||
struct.pack('<QQi', 0, self._get_new_msg_id(), len(data)) + data
|
||||
)
|
||||
|
||||
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()
|
||||
if body == b'l\xfe\xff\xff': # -404 little endian signed
|
||||
# Broken authorization, must reset the auth key
|
||||
|
@ -46,7 +65,7 @@ class MtProtoPlainSender:
|
|||
return response
|
||||
|
||||
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
|
||||
now = time.time()
|
||||
nanoseconds = int((now - int(now)) * 1e+9)
|
||||
|
|
|
@ -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 logging
|
||||
import struct
|
||||
|
@ -19,7 +23,7 @@ from ..tl.types import (
|
|||
)
|
||||
from ..tl.functions.auth import LogOutRequest
|
||||
|
||||
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
||||
__log__ = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MtProtoSender:
|
||||
|
@ -33,38 +37,50 @@ class MtProtoSender:
|
|||
"""
|
||||
|
||||
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.connection = connection
|
||||
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
|
||||
self._pending_receive = {}
|
||||
|
||||
async def connect(self):
|
||||
"""Connects to the server"""
|
||||
"""Connects to the server."""
|
||||
await self.connection.connect(self.session.server_address, self.session.port)
|
||||
|
||||
def is_connected(self):
|
||||
"""
|
||||
Determines whether the sender is connected or not.
|
||||
|
||||
:return: true if the sender is connected.
|
||||
"""
|
||||
return self.connection.is_connected()
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnects from the server"""
|
||||
"""Disconnects from the server."""
|
||||
self.connection.close()
|
||||
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
|
||||
|
||||
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
|
||||
for r in requests:
|
||||
|
@ -90,18 +106,23 @@ class MtProtoSender:
|
|||
await self._send_message(message)
|
||||
|
||||
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])))
|
||||
|
||||
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
|
||||
of the MtProtoSender such as the updates callback being fired
|
||||
or a pending request being confirmed.
|
||||
This method returns nothing, and will only affect other parts
|
||||
of the MtProtoSender such as the updates callback being fired
|
||||
or a pending request being confirmed.
|
||||
|
||||
Any unhandled object (likely updates) will be passed to
|
||||
update_state.process(TLObject).
|
||||
Any unhandled object (likely updates) will be passed to
|
||||
update_state.process(TLObject).
|
||||
|
||||
:param update_state:
|
||||
the UpdateState that will process all the received
|
||||
Update and Updates objects.
|
||||
"""
|
||||
try:
|
||||
body = await self.connection.recv()
|
||||
|
@ -113,6 +134,9 @@ class MtProtoSender:
|
|||
# "This packet should be skipped"; since this may have
|
||||
# been a result for a request, invalidate every request
|
||||
# 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()
|
||||
return
|
||||
|
||||
|
@ -126,10 +150,13 @@ class MtProtoSender:
|
|||
# region Low level processing
|
||||
|
||||
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 = \
|
||||
struct.pack('<QQ', self.session.salt, self.session.id) \
|
||||
struct.pack('<qq', self.session.salt, self.session.id) \
|
||||
+ bytes(message)
|
||||
|
||||
msg_key = utils.calc_msg_key(plain_text)
|
||||
|
@ -141,7 +168,12 @@ class MtProtoSender:
|
|||
await self.connection.send(result)
|
||||
|
||||
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
|
||||
remote_msg_id = None
|
||||
remote_sequence = None
|
||||
|
@ -172,100 +204,113 @@ class MtProtoSender:
|
|||
return message, remote_msg_id, remote_sequence
|
||||
|
||||
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
|
||||
|
||||
code = reader.read_int(signed=False)
|
||||
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)
|
||||
__log__.debug('Processing Remote Procedure Call result')
|
||||
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:
|
||||
__log__.debug('Processing container result')
|
||||
return await self._handle_container(msg_id, sequence, reader, state)
|
||||
|
||||
if code == GzipPacked.CONSTRUCTOR_ID:
|
||||
__log__.debug('Processing gzipped result')
|
||||
return await self._handle_gzip_packed(msg_id, sequence, reader, state)
|
||||
|
||||
if code == BadServerSalt.CONSTRUCTOR_ID:
|
||||
return await self._handle_bad_server_salt(msg_id, sequence, reader)
|
||||
if code not in tlobjects:
|
||||
__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:
|
||||
return await self._handle_bad_msg_notification(msg_id, sequence, reader)
|
||||
obj = reader.tgread_object()
|
||||
__log__.debug('Processing %s result', type(obj).__name__)
|
||||
|
||||
if code == MsgDetailedInfo.CONSTRUCTOR_ID:
|
||||
return await self._handle_msg_detailed_info(msg_id, sequence, reader)
|
||||
if isinstance(obj, Pong):
|
||||
return await self._handle_pong(msg_id, sequence, obj)
|
||||
|
||||
if code == MsgNewDetailedInfo.CONSTRUCTOR_ID:
|
||||
return await self._handle_msg_new_detailed_info(msg_id, sequence, reader)
|
||||
if isinstance(obj, BadServerSalt):
|
||||
return await self._handle_bad_server_salt(msg_id, sequence, obj)
|
||||
|
||||
if code == NewSessionCreated.CONSTRUCTOR_ID:
|
||||
return await self._handle_new_session_created(msg_id, sequence, reader)
|
||||
if isinstance(obj, BadMsgNotification):
|
||||
return await self._handle_bad_msg_notification(msg_id, sequence, obj)
|
||||
|
||||
if code == MsgsAck.CONSTRUCTOR_ID: # may handle the request we wanted
|
||||
ack = reader.tgread_object()
|
||||
assert isinstance(ack, MsgsAck)
|
||||
# Ignore every ack request *unless* when logging out,
|
||||
if isinstance(obj, MsgDetailedInfo):
|
||||
return await self._handle_msg_detailed_info(msg_id, sequence, obj)
|
||||
|
||||
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
|
||||
# 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)
|
||||
if r:
|
||||
r.result = True # Telegram won't send this value
|
||||
r.confirm_received.set()
|
||||
self._logger.debug('Message ack confirmed', r)
|
||||
|
||||
return True
|
||||
|
||||
# If the code is not parsed manually then it should be a TLObject.
|
||||
if code in tlobjects:
|
||||
result = reader.tgread_object()
|
||||
self.session.process_entities(result)
|
||||
if state:
|
||||
state.process(result)
|
||||
# If the object isn't any of the above, then it should be an Update.
|
||||
self.session.process_entities(obj)
|
||||
if state:
|
||||
state.process(obj)
|
||||
|
||||
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
|
||||
return True
|
||||
|
||||
# endregion
|
||||
|
||||
# region Message handling
|
||||
|
||||
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)
|
||||
if message:
|
||||
return message.request
|
||||
|
||||
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)
|
||||
if message and isinstance(message.request, t):
|
||||
return self._pending_receive.pop(msg_id).request
|
||||
|
||||
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()
|
||||
if msg.container_msg_id == container_msg_id]
|
||||
|
@ -276,13 +321,19 @@ class MtProtoSender:
|
|||
return requests
|
||||
|
||||
def _clear_all_pending(self):
|
||||
"""
|
||||
Clears all pending requests, and flags them all as received.
|
||||
"""
|
||||
for r in self._pending_receive.values():
|
||||
r.request.confirm_received.set()
|
||||
self._pending_receive.clear()
|
||||
|
||||
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)
|
||||
if request:
|
||||
|
@ -294,21 +345,31 @@ class MtProtoSender:
|
|||
self._logger.debug('Resending container of requests')
|
||||
await self.send(*requests)
|
||||
|
||||
async def _handle_pong(self, msg_id, sequence, reader):
|
||||
self._logger.debug('Handling pong')
|
||||
pong = reader.tgread_object()
|
||||
assert isinstance(pong, Pong)
|
||||
def _handle_pong(self, msg_id, sequence, pong):
|
||||
"""
|
||||
Handles a Pong response.
|
||||
|
||||
: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)
|
||||
if request:
|
||||
self._logger.debug('Pong confirmed a request')
|
||||
request.result = pong
|
||||
request.confirm_received.set()
|
||||
|
||||
return True
|
||||
|
||||
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):
|
||||
begin_position = reader.tell_position()
|
||||
|
||||
|
@ -324,15 +385,16 @@ class MtProtoSender:
|
|||
|
||||
return True
|
||||
|
||||
async def _handle_bad_server_salt(self, msg_id, sequence, reader):
|
||||
self._logger.debug('Handling bad server salt')
|
||||
bad_salt = reader.tgread_object()
|
||||
assert isinstance(bad_salt, BadServerSalt)
|
||||
async def _handle_bad_server_salt(self, msg_id, sequence, bad_salt):
|
||||
"""
|
||||
Handles a BadServerSalt response.
|
||||
|
||||
# Our salt is unsigned, but the objects work with signed salts
|
||||
self.session.salt = struct.unpack(
|
||||
'<Q', struct.pack('<q', bad_salt.new_server_salt)
|
||||
)[0]
|
||||
:param msg_id: the ID of the message.
|
||||
:param sequence: the sequence of the message.
|
||||
:param reader: the reader containing the BadServerSalt.
|
||||
:return: true, as it always succeeds.
|
||||
"""
|
||||
self.session.salt = bad_salt.new_server_salt
|
||||
self.session.save()
|
||||
|
||||
# "the bad_server_salt response is received with the
|
||||
|
@ -341,60 +403,91 @@ class MtProtoSender:
|
|||
|
||||
return True
|
||||
|
||||
async def _handle_bad_msg_notification(self, msg_id, sequence, reader):
|
||||
self._logger.debug('Handling bad message notification')
|
||||
bad_msg = reader.tgread_object()
|
||||
assert isinstance(bad_msg, BadMsgNotification)
|
||||
async def _handle_bad_msg_notification(self, msg_id, sequence, bad_msg):
|
||||
"""
|
||||
Handles a BadMessageError response.
|
||||
|
||||
: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)
|
||||
__log__.warning('Read bad msg notification %s: %s', bad_msg, error)
|
||||
if bad_msg.error_code in (16, 17):
|
||||
# sent msg_id too low or too high (respectively).
|
||||
# Use the current msg_id to determine the right time offset.
|
||||
self.session.update_time_offset(correct_msg_id=msg_id)
|
||||
self._logger.debug('Read Bad Message error: ' + str(error))
|
||||
self._logger.debug('Attempting to use the correct time offset.')
|
||||
__log__.info('Attempting to use the correct time offset')
|
||||
await self._resend_request(bad_msg.bad_msg_id)
|
||||
return True
|
||||
elif bad_msg.error_code == 32:
|
||||
# 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
|
||||
self.session._sequence += 64
|
||||
__log__.info('Attempting to set the right higher sequence')
|
||||
await self._resend_request(bad_msg.bad_msg_id)
|
||||
return True
|
||||
elif bad_msg.error_code == 33:
|
||||
# msg_seqno too high never seems to happen but just in case
|
||||
self.session._sequence -= 16
|
||||
__log__.info('Attempting to set the right lower sequence')
|
||||
await self._resend_request(bad_msg.bad_msg_id)
|
||||
return True
|
||||
else:
|
||||
raise error
|
||||
|
||||
async def _handle_msg_detailed_info(self, msg_id, sequence, reader):
|
||||
msg_new = reader.tgread_object()
|
||||
assert isinstance(msg_new, MsgDetailedInfo)
|
||||
async def _handle_msg_detailed_info(self, msg_id, sequence, msg_new):
|
||||
"""
|
||||
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
|
||||
# Relevant tdesktop source code: https://goo.gl/VvpCC6
|
||||
await self._send_acknowledge(msg_new.answer_msg_id)
|
||||
return True
|
||||
|
||||
async def _handle_msg_new_detailed_info(self, msg_id, sequence, reader):
|
||||
msg_new = reader.tgread_object()
|
||||
assert isinstance(msg_new, MsgNewDetailedInfo)
|
||||
async def _handle_msg_new_detailed_info(self, msg_id, sequence, msg_new):
|
||||
"""
|
||||
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
|
||||
# Relevant tdesktop source code: https://goo.gl/G7DPsR
|
||||
await self._send_acknowledge(msg_new.answer_msg_id)
|
||||
return True
|
||||
|
||||
async def _handle_new_session_created(self, msg_id, sequence, reader):
|
||||
new_session = reader.tgread_object()
|
||||
assert isinstance(new_session, NewSessionCreated)
|
||||
async def _handle_new_session_created(self, msg_id, sequence, new_session):
|
||||
"""
|
||||
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
|
||||
return True
|
||||
|
||||
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
|
||||
request_id = reader.read_long()
|
||||
inner_code = reader.read_int(signed=False)
|
||||
|
@ -417,11 +510,9 @@ class MtProtoSender:
|
|||
request.confirm_received.set()
|
||||
# else TODO Where should this error be reported?
|
||||
# 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
|
||||
|
||||
elif request:
|
||||
self._logger.debug('Reading request response')
|
||||
if inner_code == 0x3072cfa1: # GZip packed
|
||||
unpacked_data = gzip.decompress(reader.tgread_bytes())
|
||||
with BinaryReader(unpacked_data) as compressed_reader:
|
||||
|
@ -436,11 +527,18 @@ class MtProtoSender:
|
|||
|
||||
# If it's really a result for RPC from previous connection
|
||||
# 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
|
||||
|
||||
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:
|
||||
return await self._process_msg(msg_id, sequence, compressed_reader, state)
|
||||
|
||||
|
|
|
@ -36,10 +36,13 @@ from .update_state import UpdateState
|
|||
from .utils import get_appropriated_part_size
|
||||
|
||||
|
||||
DEFAULT_DC_ID = 4
|
||||
DEFAULT_IPV4_IP = '149.154.167.51'
|
||||
DEFAULT_IPV6_IP = '[2001:67c:4e8:f002::a]'
|
||||
DEFAULT_PORT = 443
|
||||
|
||||
__log__ = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TelegramBareClient:
|
||||
"""Bare Telegram Client with just the minimum -
|
||||
|
@ -78,17 +81,17 @@ class TelegramBareClient:
|
|||
**kwargs):
|
||||
"""Refer to TelegramClient.__init__ for docs on this method"""
|
||||
if not api_id or not api_hash:
|
||||
raise PermissionError(
|
||||
raise ValueError(
|
||||
"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
|
||||
|
||||
# Determine what session object we have
|
||||
if isinstance(session, str) or session is None:
|
||||
session = Session.try_load_or_create_new(session)
|
||||
session = Session(session)
|
||||
elif not isinstance(session, Session):
|
||||
raise ValueError(
|
||||
raise TypeError(
|
||||
'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
|
||||
if (not session.server_address or
|
||||
(':' in session.server_address) != use_ipv6):
|
||||
session.port = DEFAULT_PORT
|
||||
session.server_address = \
|
||||
DEFAULT_IPV6_IP if self._use_ipv6 else DEFAULT_IPV4_IP
|
||||
session.set_dc(
|
||||
DEFAULT_DC_ID,
|
||||
DEFAULT_IPV6_IP if self._use_ipv6 else DEFAULT_IPV4_IP,
|
||||
DEFAULT_PORT
|
||||
)
|
||||
|
||||
self.session = session
|
||||
self.api_id = int(api_id)
|
||||
|
@ -117,10 +122,8 @@ class TelegramBareClient:
|
|||
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)
|
||||
|
||||
# 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)
|
||||
self._authorized = None # None = We don't know yet
|
||||
|
||||
# Uploaded files cache so subsequent calls are instant
|
||||
self._upload_cache = {}
|
||||
# The first request must be in invokeWithLayer(initConnection(X)).
|
||||
# See https://core.telegram.org/api/invoking#saving-client-info.
|
||||
self._first_request = True
|
||||
|
||||
self._recv_loop = None
|
||||
self._ping_loop = None
|
||||
|
@ -178,8 +182,12 @@ class TelegramBareClient:
|
|||
native data center, raising a "UserMigrateError", and
|
||||
calling .disconnect() in the process.
|
||||
"""
|
||||
__log__.info('Connecting to %s:%d...',
|
||||
self.session.server_address, self.session.port)
|
||||
|
||||
try:
|
||||
await self._sender.connect()
|
||||
__log__.info('Connection success!')
|
||||
|
||||
# Connection was successful! Try syncing the update state
|
||||
# UNLESS '_sync_updates' is False (we probably are in
|
||||
|
@ -199,14 +207,15 @@ class TelegramBareClient:
|
|||
|
||||
except TypeNotFoundError as e:
|
||||
# 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()
|
||||
return await self.connect(_sync_updates=_sync_updates)
|
||||
|
||||
except (RPCError, ConnectionError):
|
||||
except (RPCError, ConnectionError) as e:
|
||||
# Probably errors from the previous session, ignore them
|
||||
__log__.error('Connection failed due to %s', e)
|
||||
self.disconnect()
|
||||
self._logger.exception('Could not stabilise initial connection.')
|
||||
return False
|
||||
|
||||
def is_connected(self):
|
||||
|
@ -227,10 +236,14 @@ class TelegramBareClient:
|
|||
|
||||
def disconnect(self):
|
||||
"""Disconnects from the Telegram server"""
|
||||
__log__.info('Disconnecting...')
|
||||
self._user_connected = False
|
||||
self._sender.disconnect()
|
||||
# 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):
|
||||
"""If 'new_dc' is not set, only a call to .connect() will be made
|
||||
|
@ -246,10 +259,13 @@ class TelegramBareClient:
|
|||
try:
|
||||
await self._reconnect_lock.acquire()
|
||||
if self.is_connected():
|
||||
__log__.info('Reconnection aborted: already connected')
|
||||
return True
|
||||
|
||||
__log__.info('Attempting reconnection...')
|
||||
return await self.connect()
|
||||
except ConnectionResetError:
|
||||
__log__.warning('Reconnection failed due to %s', e)
|
||||
return False
|
||||
finally:
|
||||
self._reconnect_lock.release()
|
||||
|
@ -258,9 +274,9 @@ class TelegramBareClient:
|
|||
# we need to first know the Data Centers we can connect to. Do
|
||||
# that before disconnecting.
|
||||
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.port = dc.port
|
||||
self.session.set_dc(dc.id, dc.ip_address, dc.port)
|
||||
# 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.
|
||||
self.session.auth_key = None
|
||||
|
@ -268,6 +284,13 @@ class TelegramBareClient:
|
|||
self.disconnect()
|
||||
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
|
||||
|
||||
# region Working with different connections/Data Centers
|
||||
|
@ -315,6 +338,7 @@ class TelegramBareClient:
|
|||
dc = await self._get_dc(dc_id)
|
||||
|
||||
# Export the current authorization to the new DC.
|
||||
__log__.info('Exporting authorization for data center %s', dc)
|
||||
export_auth = await self(ExportAuthorizationRequest(dc_id))
|
||||
|
||||
# Create a temporary session for this IP address, which needs
|
||||
|
@ -323,10 +347,10 @@ class TelegramBareClient:
|
|||
# Construct this session with the connection parameters
|
||||
# (system version, device model...) from the current one.
|
||||
session = Session(self.session)
|
||||
session.server_address = dc.ip_address
|
||||
session.port = dc.port
|
||||
session.set_dc(dc.id, dc.ip_address, dc.port)
|
||||
self._exported_sessions[dc_id] = session
|
||||
|
||||
__log__.info('Creating exported new client')
|
||||
client = TelegramBareClient(
|
||||
session, self.api_id, self.api_hash,
|
||||
proxy=self._sender.connection.conn.proxy,
|
||||
|
@ -339,7 +363,7 @@ class TelegramBareClient:
|
|||
id=export_auth.id, bytes=export_auth.bytes
|
||||
))
|
||||
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
|
||||
return client
|
||||
|
@ -350,10 +374,10 @@ class TelegramBareClient:
|
|||
if not session:
|
||||
dc = await self._get_dc(cdn_redirect.dc_id, cdn=True)
|
||||
session = Session(self.session)
|
||||
session.server_address = dc.ip_address
|
||||
session.port = dc.port
|
||||
session.set_dc(dc.id, dc.ip_address, dc.port)
|
||||
self._exported_sessions[cdn_redirect.dc_id] = session
|
||||
|
||||
__log__.info('Creating new CDN client')
|
||||
client = TelegramBareClient(
|
||||
session, self.api_id, self.api_hash,
|
||||
proxy=self._sender.connection.conn.proxy,
|
||||
|
@ -378,11 +402,20 @@ class TelegramBareClient:
|
|||
"""Invokes (sends) a MTProtoRequest and returns (receives) its result.
|
||||
|
||||
The invoke will be retried up to 'retries' times before raising
|
||||
ValueError().
|
||||
RuntimeError().
|
||||
"""
|
||||
if not all(isinstance(x, TLObject) and
|
||||
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
|
||||
# thread reading or if the server disconnected us and we're trying
|
||||
|
@ -395,35 +428,34 @@ class TelegramBareClient:
|
|||
if result is not None:
|
||||
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)
|
||||
self._logger.debug('RPC failed. Attempting reconnection.')
|
||||
if not self._reconnect_lock.locked():
|
||||
with await self._reconnect_lock:
|
||||
self._reconnect()
|
||||
|
||||
raise ValueError('Number of retries reached 0.')
|
||||
raise RuntimeError('Number of retries reached 0.')
|
||||
|
||||
# Let people use client.invoke(SomeRequest()) instead client(...)
|
||||
invoke = __call__
|
||||
|
||||
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:
|
||||
# Ensure that we start with no previous errors (i.e. resending)
|
||||
for x in requests:
|
||||
x.rpc_error = None
|
||||
|
||||
if not self.session.auth_key:
|
||||
# New key, we need to tell the server we're going to use
|
||||
# the latest layer and initialize the connection doing so.
|
||||
__log__.info('Need to generate new auth key before invoking')
|
||||
self._first_request = True
|
||||
self.session.auth_key, self.session.time_offset = \
|
||||
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:
|
||||
requests = [self._wrap_init_connection(requests[0])]
|
||||
else:
|
||||
|
@ -447,13 +479,14 @@ class TelegramBareClient:
|
|||
await self._sender.receive(update_state=self.updates)
|
||||
|
||||
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
|
||||
|
||||
except TimeoutError:
|
||||
pass # We will just retry
|
||||
__log__.warning('Invoking timed out') # We will just retry
|
||||
|
||||
except ConnectionResetError:
|
||||
__log__.warning('Connection was reset while invoking')
|
||||
if self._user_connected:
|
||||
# Server disconnected us, __call__ will try reconnecting.
|
||||
return None
|
||||
|
@ -461,11 +494,8 @@ class TelegramBareClient:
|
|||
# User never called .connect(), so raise this error.
|
||||
raise
|
||||
|
||||
if init_connection:
|
||||
# We initialized the connection successfully, even if
|
||||
# a request had an RPC error we have invoked it fine.
|
||||
self.session.layer = LAYER
|
||||
self.session.save()
|
||||
# Clear the flag if we got this far
|
||||
self._first_request = False
|
||||
|
||||
try:
|
||||
raise next(x.rpc_error for x in requests if x.rpc_error)
|
||||
|
@ -482,27 +512,19 @@ class TelegramBareClient:
|
|||
|
||||
except (PhoneMigrateError, NetworkMigrateError,
|
||||
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)
|
||||
return None
|
||||
|
||||
except ServerError as e:
|
||||
# Telegram is having some issues, just retry
|
||||
self._logger.debug(
|
||||
'[ERROR] Telegram is having some internal issues', e
|
||||
)
|
||||
__log__.error('Telegram servers are having internal errors %s', e)
|
||||
|
||||
except FloodWaitError as e:
|
||||
__log__.warning('Request invoked too often, wait %ds', e.seconds)
|
||||
if e.seconds > self.session.flood_sleep_threshold | 0:
|
||||
raise
|
||||
|
||||
self._logger.debug(
|
||||
'Sleep of %d seconds below threshold, sleeping' % e.seconds
|
||||
)
|
||||
await asyncio.sleep(e.seconds, loop=self._loop)
|
||||
return None
|
||||
|
||||
|
@ -540,6 +562,9 @@ class TelegramBareClient:
|
|||
part_size_kb = get_appropriated_part_size(file_size)
|
||||
file_name = os.path.basename(file_path)
|
||||
"""
|
||||
if isinstance(file, (InputFile, InputFileBig)):
|
||||
return file # Already uploaded
|
||||
|
||||
if isinstance(file, str):
|
||||
file_size = os.path.getsize(file)
|
||||
elif isinstance(file, bytes):
|
||||
|
@ -548,6 +573,7 @@ class TelegramBareClient:
|
|||
file = file.read()
|
||||
file_size = len(file)
|
||||
|
||||
# File will now either be a string or bytes
|
||||
if not part_size_kb:
|
||||
part_size_kb = get_appropriated_part_size(file_size)
|
||||
|
||||
|
@ -558,16 +584,40 @@ class TelegramBareClient:
|
|||
if part_size % 1024 != 0:
|
||||
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
|
||||
# Telegram does make a distinction between smaller or larger files
|
||||
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
|
||||
__log__.info('Uploading file of %d bytes in %d chunks of %d',
|
||||
file_size, part_count, part_size)
|
||||
|
||||
file_id = utils.generate_random_long()
|
||||
hash_md5 = md5()
|
||||
|
||||
stream = open(file, 'rb') if isinstance(file, str) else BytesIO(file)
|
||||
try:
|
||||
with open(file, 'rb') if isinstance(file, str) else BytesIO(file) \
|
||||
as stream:
|
||||
for part_index in range(part_count):
|
||||
# Read the file by in chunks of size part_size
|
||||
part = stream.read(part_size)
|
||||
|
@ -582,28 +632,19 @@ class TelegramBareClient:
|
|||
|
||||
result = await self(request)
|
||||
if result:
|
||||
if not is_large:
|
||||
# No need to update the hash if it's a large file
|
||||
hash_md5.update(part)
|
||||
|
||||
__log__.debug('Uploaded %d/%d', part_index + 1, part_count)
|
||||
if progress_callback:
|
||||
progress_callback(stream.tell(), file_size)
|
||||
else:
|
||||
raise ValueError('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)
|
||||
raise RuntimeError(
|
||||
'Failed to upload file part {}.'.format(part_index))
|
||||
|
||||
if is_large:
|
||||
return InputFileBig(file_id, part_count, file_name)
|
||||
else:
|
||||
self.session.cache_file(
|
||||
hash_md5.digest(), file_size, file_id, part_count)
|
||||
|
||||
return InputFile(file_id, part_count, file_name,
|
||||
md5_checksum=hash_md5.hexdigest())
|
||||
|
||||
|
@ -650,6 +691,7 @@ class TelegramBareClient:
|
|||
client = self
|
||||
cdn_decrypter = None
|
||||
|
||||
__log__.info('Downloading file in chunks of %d bytes', part_size)
|
||||
try:
|
||||
offset = 0
|
||||
while True:
|
||||
|
@ -662,6 +704,7 @@ class TelegramBareClient:
|
|||
))
|
||||
|
||||
if isinstance(result, FileCdnRedirect):
|
||||
__log__.info('File lives in a CDN')
|
||||
cdn_decrypter, result = \
|
||||
await CdnDecrypter.prepare_decrypter(
|
||||
client,
|
||||
|
@ -670,6 +713,7 @@ class TelegramBareClient:
|
|||
)
|
||||
|
||||
except FileMigrateError as e:
|
||||
__log__.info('File lives in another DC')
|
||||
client = await self._get_exported_client(e.new_dc)
|
||||
continue
|
||||
|
||||
|
@ -682,6 +726,7 @@ class TelegramBareClient:
|
|||
return getattr(result, 'type', '')
|
||||
|
||||
f.write(result.bytes)
|
||||
__log__.debug('Saved %d more bytes', len(result.bytes))
|
||||
if progress_callback:
|
||||
progress_callback(f.tell(), file_size)
|
||||
finally:
|
||||
|
@ -736,28 +781,30 @@ class TelegramBareClient:
|
|||
self._ping_loop = None
|
||||
|
||||
async def _recv_loop_impl(self):
|
||||
__log__.info('Starting to wait for items from the network')
|
||||
need_reconnect = False
|
||||
while self._user_connected:
|
||||
try:
|
||||
if need_reconnect:
|
||||
__log__.info('Attempting reconnection from read loop')
|
||||
need_reconnect = False
|
||||
while self._user_connected and not await self._reconnect():
|
||||
# Retry forever, this is instant messaging
|
||||
await asyncio.sleep(0.1, loop=self._loop)
|
||||
|
||||
__log__.debug('Receiving items from the network...')
|
||||
await self._sender.receive(update_state=self.updates)
|
||||
except TimeoutError:
|
||||
# No problem.
|
||||
pass
|
||||
__log__.info('Receiving items from the network timed out')
|
||||
except ConnectionError as error:
|
||||
self._logger.debug(error)
|
||||
need_reconnect = True
|
||||
__log__.error('Connection was reset while receiving items')
|
||||
await asyncio.sleep(1, loop=self._loop)
|
||||
except Exception as error:
|
||||
# Unknown exception, pass it to the main thread
|
||||
self._logger.exception(
|
||||
'Unknown error on the read loop, please report.'
|
||||
)
|
||||
__log__.exception('Unknown exception in the read thread! '
|
||||
'Disconnecting and leaving it to main thread')
|
||||
|
||||
try:
|
||||
import socks
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1 +1,2 @@
|
|||
from .draft import Draft
|
||||
from .dialog import Dialog
|
||||
|
|
37
telethon/tl/custom/dialog.py
Normal file
37
telethon/tl/custom/dialog.py
Normal 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)
|
|
@ -21,7 +21,7 @@ class Draft:
|
|||
@classmethod
|
||||
def _from_update(cls, client, update):
|
||||
if not isinstance(update, UpdateDraftMessage):
|
||||
raise ValueError(
|
||||
raise TypeError(
|
||||
'You can only create a new `Draft` from a corresponding '
|
||||
'`UpdateDraftMessage` object.'
|
||||
)
|
||||
|
@ -29,14 +29,14 @@ class Draft:
|
|||
return cls(client=client, peer=update.peer, draft=update.draft)
|
||||
|
||||
@property
|
||||
def entity(self):
|
||||
return self._client.get_entity(self._peer)
|
||||
async def entity(self):
|
||||
return await self._client.get_entity(self._peer)
|
||||
|
||||
@property
|
||||
def input_entity(self):
|
||||
return self._client.get_input_entity(self._peer)
|
||||
async def input_entity(self):
|
||||
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
|
||||
reflected in this object. Changing only individual attributes like for
|
||||
|
@ -56,7 +56,7 @@ class Draft:
|
|||
:param list entities: A list of formatting entities
|
||||
:return bool: `True` on success
|
||||
"""
|
||||
result = self._client(SaveDraftRequest(
|
||||
result = await self._client(SaveDraftRequest(
|
||||
peer=self._peer,
|
||||
message=text,
|
||||
no_webpage=no_webpage,
|
||||
|
@ -72,9 +72,9 @@ class Draft:
|
|||
|
||||
return result
|
||||
|
||||
def delete(self):
|
||||
async def delete(self):
|
||||
"""
|
||||
Deletes this draft
|
||||
:return bool: `True` on success
|
||||
"""
|
||||
return self.set_message(text='')
|
||||
return await self.set_message(text='')
|
||||
|
|
|
@ -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]
|
|
@ -1,12 +1,20 @@
|
|||
import json
|
||||
import os
|
||||
import platform
|
||||
import sqlite3
|
||||
import time
|
||||
from base64 import b64encode, b64decode
|
||||
from base64 import b64decode
|
||||
from os.path import isfile as file_exists
|
||||
|
||||
from .entity_database import EntityDatabase
|
||||
from .. import helpers
|
||||
from .. import utils, helpers
|
||||
from ..tl import TLObject
|
||||
from ..tl.types import (
|
||||
PeerUser, PeerChat, PeerChannel,
|
||||
InputPeerUser, InputPeerChat, InputPeerChannel
|
||||
)
|
||||
|
||||
EXTENSION = '.session'
|
||||
CURRENT_VERSION = 2 # database version
|
||||
|
||||
|
||||
class Session:
|
||||
|
@ -17,33 +25,34 @@ class Session:
|
|||
If you think the session has been compromised, close all the sessions
|
||||
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.
|
||||
Note that if another session is given, only parameters like
|
||||
those required to init a connection will be copied.
|
||||
"""
|
||||
# These values will NOT be saved
|
||||
if isinstance(session_user_id, Session):
|
||||
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
|
||||
self.filename = ':memory:'
|
||||
|
||||
# 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
|
||||
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()
|
||||
self.device_model = system.system if system.system else 'Unknown'
|
||||
self.system_version = system.release if system.release else '1.0'
|
||||
self.device_model = system.system or 'Unknown'
|
||||
self.system_version = system.release or '1.0'
|
||||
self.app_version = '1.0' # '0' will provoke error
|
||||
self.lang_code = 'en'
|
||||
self.system_lang_code = self.lang_code
|
||||
|
@ -52,43 +61,177 @@ class Session:
|
|||
self.save_entities = True
|
||||
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.time_offset = 0
|
||||
self._last_msg_id = 0 # Long
|
||||
self.salt = 0 # Long
|
||||
|
||||
# These values will be saved
|
||||
self.server_address = None
|
||||
self.port = None
|
||||
self.auth_key = None
|
||||
self.layer = 0
|
||||
self.salt = 0 # Unsigned long
|
||||
self.entities = EntityDatabase() # Known and cached entities
|
||||
self._dc_id = 0
|
||||
self._server_address = None
|
||||
self._port = None
|
||||
self._auth_key = None
|
||||
|
||||
# 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):
|
||||
"""Saves the current session object as session_user_id.session"""
|
||||
if not self.session_user_id:
|
||||
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)
|
||||
self._conn.commit()
|
||||
|
||||
def delete(self):
|
||||
"""Deletes the current session file"""
|
||||
if self.filename == ':memory:':
|
||||
return True
|
||||
try:
|
||||
os.remove('{}.session'.format(self.session_user_id))
|
||||
os.remove(self.filename)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
@ -99,43 +242,7 @@ class Session:
|
|||
using this client and never logged out
|
||||
"""
|
||||
return [os.path.splitext(os.path.basename(f))[0]
|
||||
for f in os.listdir('.') if f.endswith('.session')]
|
||||
|
||||
@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
|
||||
for f in os.listdir('.') if f.endswith(EXTENSION)]
|
||||
|
||||
def generate_sequence(self, content_related):
|
||||
"""Thread safe method to generates the next sequence number,
|
||||
|
@ -173,9 +280,119 @@ class Session:
|
|||
correct = correct_msg_id >> 32
|
||||
self.time_offset = correct - now
|
||||
|
||||
def process_entities(self, tlobject):
|
||||
try:
|
||||
if self.entities.process(tlobject):
|
||||
self.save() # Save if any new entities got added
|
||||
except:
|
||||
pass
|
||||
# Entity processing
|
||||
|
||||
def process_entities(self, tlo):
|
||||
"""Processes all the found entities on the given TLObject,
|
||||
unless .enabled is False.
|
||||
|
||||
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()
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
from datetime import datetime
|
||||
import struct
|
||||
from datetime import datetime, date
|
||||
|
||||
|
||||
class TLObject:
|
||||
def __init__(self):
|
||||
self.request_msg_id = 0 # Long
|
||||
self.confirm_received = None
|
||||
self.rpc_error = None
|
||||
|
||||
|
@ -97,7 +97,8 @@ class TLObject:
|
|||
if isinstance(data, str):
|
||||
data = data.encode('utf-8')
|
||||
else:
|
||||
raise ValueError('bytes or str expected, not', type(data))
|
||||
raise TypeError(
|
||||
'bytes or str expected, not {}'.format(type(data)))
|
||||
|
||||
r = []
|
||||
if len(data) < 254:
|
||||
|
@ -124,6 +125,23 @@ class TLObject:
|
|||
r.append(bytes(padding))
|
||||
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
|
||||
def to_dict(self, recursive=True):
|
||||
return {}
|
||||
|
|
|
@ -6,6 +6,8 @@ from datetime import datetime
|
|||
|
||||
from .tl import types as tl
|
||||
|
||||
__log__ = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UpdateState:
|
||||
"""Used to hold the current state of processed updates.
|
||||
|
@ -32,11 +34,13 @@ class UpdateState:
|
|||
the library itself.
|
||||
"""
|
||||
if isinstance(update, tl.updates.State):
|
||||
__log__.debug('Saved new updates state')
|
||||
self._state = update
|
||||
return # Nothing else to be done
|
||||
|
||||
pts = getattr(update, '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
|
||||
|
||||
self._state.pts = pts
|
||||
|
@ -57,28 +61,21 @@ class UpdateState:
|
|||
"""
|
||||
data = pickle.dumps(update.to_dict())
|
||||
if data in self._latest_updates:
|
||||
__log__.info('Ignoring %s, already have it', update)
|
||||
return # Duplicated too
|
||||
|
||||
self._latest_updates.append(data)
|
||||
|
||||
if type(update).SUBCLASS_OF_ID == 0x8af52aac: # crc32(b'Updates')
|
||||
# Expand "Updates" into "Update", and pass these to callbacks.
|
||||
# Since .users and .chats have already been processed, we
|
||||
# don't need to care about those either.
|
||||
if isinstance(update, tl.UpdateShort):
|
||||
self.handle_update(update.update)
|
||||
# Expand "Updates" into "Update", and pass these to callbacks.
|
||||
# Since .users and .chats have already been processed, we
|
||||
# don't need to care about those either.
|
||||
if isinstance(update, tl.UpdateShort):
|
||||
self.handle_update(update.update)
|
||||
|
||||
elif isinstance(update, (tl.Updates, tl.UpdatesCombined)):
|
||||
for upd in update.updates:
|
||||
self.handle_update(upd)
|
||||
elif isinstance(update, (tl.Updates, tl.UpdatesCombined)):
|
||||
for upd in update.updates:
|
||||
self.handle_update(upd)
|
||||
|
||||
elif not isinstance(update, tl.UpdatesTooLong):
|
||||
# TODO Handle "Updates too long"
|
||||
self.handle_update(update)
|
||||
|
||||
elif type(update).SUBCLASS_OF_ID == 0x9f89304e: # crc32(b'Update')
|
||||
self.handle_update(update)
|
||||
# TODO Handle "Updates too long"
|
||||
else:
|
||||
self._logger.debug('Ignoring "update" of type {}'.format(
|
||||
type(update).__name__)
|
||||
)
|
||||
self.handle_update(update)
|
||||
|
|
|
@ -5,6 +5,8 @@ to convert between an entity like an User, Chat, etc. into its Input version)
|
|||
import math
|
||||
from mimetypes import add_type, guess_extension
|
||||
|
||||
import re
|
||||
|
||||
from .tl import TLObject
|
||||
from .tl.types import (
|
||||
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):
|
||||
"""Gets the input peer for the given "entity" (user, chat or channel)
|
||||
Returns None if it was not found"""
|
||||
|
@ -35,12 +42,12 @@ def get_display_name(entity):
|
|||
elif entity.last_name:
|
||||
return entity.last_name
|
||||
else:
|
||||
return '(No name)'
|
||||
return ''
|
||||
|
||||
if isinstance(entity, (Chat, Channel)):
|
||||
elif isinstance(entity, (Chat, Channel)):
|
||||
return entity.title
|
||||
|
||||
return '(unknown)'
|
||||
return ''
|
||||
|
||||
# For some reason, .webp (stickers' format) is not registered
|
||||
add_type('image/webp', '.webp')
|
||||
|
@ -67,13 +74,13 @@ def get_extension(media):
|
|||
|
||||
|
||||
def _raise_cast_fail(entity, target):
|
||||
raise ValueError('Cannot cast {} to any kind of {}.'
|
||||
.format(type(entity).__name__, target))
|
||||
raise TypeError('Cannot cast {} to any kind of {}.'.format(
|
||||
type(entity).__name__, target))
|
||||
|
||||
|
||||
def get_input_peer(entity, allow_self=True):
|
||||
"""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):
|
||||
_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')
|
||||
|
||||
|
||||
def get_peer_id(peer, add_mark=False):
|
||||
"""Finds the ID of the given peer, and optionally converts it to
|
||||
the "bot api" format if 'add_mark' is set to True.
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
if not isinstance(peer, TLObject):
|
||||
|
@ -324,7 +362,7 @@ def get_peer_id(peer, add_mark=False):
|
|||
if isinstance(peer, (PeerUser, InputPeerUser)):
|
||||
return peer.user_id
|
||||
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)):
|
||||
if isinstance(peer, ChannelFull):
|
||||
# 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
|
||||
else:
|
||||
i = peer.channel_id
|
||||
if add_mark:
|
||||
# Concat -100 through math tricks, .to_supergroup() on Madeline
|
||||
# IDs will be strictly positive -> log works
|
||||
return -(i + pow(10, math.floor(math.log10(i) + 3)))
|
||||
else:
|
||||
return i
|
||||
# Concat -100 through math tricks, .to_supergroup() on Madeline
|
||||
# IDs will be strictly positive -> log works
|
||||
return -(i + pow(10, math.floor(math.log10(i) + 3)))
|
||||
|
||||
_raise_cast_fail(peer, 'int')
|
||||
|
||||
|
@ -353,28 +388,6 @@ def resolve_id(marked_id):
|
|||
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):
|
||||
"""Gets the appropriated part size when uploading or downloading files,
|
||||
given an initial file size"""
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
# Versions should comply with PEP440.
|
||||
# This line is parsed in setup.py:
|
||||
__version__ = '0.15.5'
|
||||
__version__ = '0.16'
|
||||
|
|
|
@ -138,15 +138,15 @@ class InteractiveTelegramClient(TelegramClient):
|
|||
|
||||
# Entities represent the user, chat or channel
|
||||
# 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
|
||||
while i is None:
|
||||
print_title('Dialogs window')
|
||||
|
||||
# Display them so the user can choose
|
||||
for i, entity in enumerate(entities, start=1):
|
||||
sprint('{}. {}'.format(i, get_display_name(entity)))
|
||||
for i, dialog in enumerate(dialogs, start=1):
|
||||
sprint('{}. {}'.format(i, get_display_name(dialog.entity)))
|
||||
|
||||
# Let the user decide who they want to talk to
|
||||
print()
|
||||
|
@ -177,7 +177,7 @@ class InteractiveTelegramClient(TelegramClient):
|
|||
i = None
|
||||
|
||||
# Retrieve the selected user (or chat, or channel)
|
||||
entity = entities[i]
|
||||
entity = dialogs[i].entity
|
||||
|
||||
# Show some information
|
||||
print_title('Chat with "{}"'.format(get_display_name(entity)))
|
||||
|
|
|
@ -17,11 +17,13 @@ class TLParser:
|
|||
|
||||
# Read all the lines from the .tl 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()
|
||||
|
||||
# Ensure that the line is not a comment
|
||||
if line and not line.startswith('//'):
|
||||
|
||||
if line:
|
||||
# Check whether the line is a type change
|
||||
# (types <-> functions) or not
|
||||
match = re.match('---(\w+)---', line)
|
||||
|
|
|
@ -166,11 +166,9 @@ inputMediaGifExternal#4843b0fd url:string q:string = 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;
|
||||
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;
|
||||
|
||||
inputSingleMedia#5eaa7809 media:InputMedia random_id:long = InputSingleMedia;
|
||||
|
||||
inputChatPhotoEmpty#1ca48f57 = InputChatPhoto;
|
||||
inputChatUploadedPhoto#927c55b4 file:InputFile = 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.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.messagesNotModified#74535f21 count:int = messages.Messages;
|
||||
|
||||
messages.chats#64ff9fd5 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;
|
||||
inputMessagesFilterVideo#9fc00e65 = MessagesFilter;
|
||||
inputMessagesFilterPhotoVideo#56e9f0e4 = MessagesFilter;
|
||||
inputMessagesFilterPhotoVideoDocuments#d95e73bb = MessagesFilter;
|
||||
inputMessagesFilterDocument#9eddf188 = MessagesFilter;
|
||||
inputMessagesFilterUrl#7ef0dd87 = MessagesFilter;
|
||||
inputMessagesFilterGif#ffc86587 = MessagesFilter;
|
||||
|
@ -368,8 +366,8 @@ inputMessagesFilterPhoneCalls#80c99768 flags:# missed:flags.0?true = MessagesFil
|
|||
inputMessagesFilterRoundVoice#7a7c17a4 = MessagesFilter;
|
||||
inputMessagesFilterRoundVideo#b549da53 = MessagesFilter;
|
||||
inputMessagesFilterMyMentions#c1f8e69a = MessagesFilter;
|
||||
inputMessagesFilterContacts#e062db83 = MessagesFilter;
|
||||
inputMessagesFilterGeo#e7026d0d = MessagesFilter;
|
||||
inputMessagesFilterContacts#e062db83 = MessagesFilter;
|
||||
|
||||
updateNewMessage#1f2b0afd message:Message pts:int pts_count:int = 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;
|
||||
|
||||
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;
|
||||
|
||||
|
@ -524,7 +522,7 @@ sendMessageGamePlayAction#dd6a8f48 = SendMessageAction;
|
|||
sendMessageRecordRoundAction#88f27fbc = 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;
|
||||
inputPrivacyKeyChatInvite#bdfb0426 = InputPrivacyKey;
|
||||
|
@ -723,7 +721,7 @@ auth.sentCodeTypeSms#c000bba2 length:int = auth.SentCodeType;
|
|||
auth.sentCodeTypeCall#5353e5a7 length:int = 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;
|
||||
|
||||
|
@ -825,7 +823,7 @@ dataJSON#7d748d04 data:string = DataJSON;
|
|||
|
||||
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;
|
||||
|
||||
|
@ -856,6 +854,8 @@ payments.savedInfo#fb8fe43c flags:# has_saved_credentials:flags.1?true saved_inf
|
|||
|
||||
inputPaymentCredentialsSaved#c10eb2cf id:string tmp_password:bytes = 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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
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;
|
||||
|
||||
|
@ -927,13 +927,15 @@ cdnFileHash#77eec38f offset:int limit:int hash:bytes = CdnFileHash;
|
|||
messages.favedStickersNotModified#9e8fa6d3 = 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;
|
||||
recentMeUrlChat#a01b22f9 url:string chat_id:int = RecentMeUrl;
|
||||
recentMeUrlStickerSet#bc0a57dc url:string set:StickerSetCovered = 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---
|
||||
|
||||
|
@ -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.dropTempAuthKeys#8e48a188 except_auth_keys:Vector<long> = Bool;
|
||||
|
||||
account.registerDevice#637ea878 token_type:int token:string = Bool;
|
||||
account.unregisterDevice#65c55b40 token_type:int token:string = Bool;
|
||||
account.registerDevice#f75874d1 token_type:int token:string other_uids:Vector<int> = Bool;
|
||||
account.unregisterDevice#3076c4bf token_type:int token:string other_uids:Vector<int> = Bool;
|
||||
account.updateNotifySettings#84be5b93 peer:InputNotifyPeer settings:InputPeerNotifySettings = Bool;
|
||||
account.getNotifySettings#12b3ad31 peer:InputNotifyPeer = PeerNotifySettings;
|
||||
account.resetNotifySettings#db7e1747 = Bool;
|
||||
|
@ -1010,7 +1012,7 @@ contacts.resetSaved#879537f1 = Bool;
|
|||
|
||||
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.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.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;
|
||||
|
@ -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.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.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.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;
|
||||
|
@ -1098,9 +1100,10 @@ messages.sendScreenshotNotification#c97df020 peer:InputPeer reply_to_msg_id:int
|
|||
messages.getFavedStickers#21ce0b0e hash:int = messages.FavedStickers;
|
||||
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.getRecentLocations#249431e2 peer:InputPeer limit:int = messages.Messages;
|
||||
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.uploadEncryptedFile#5057c497 peer:InputEncryptedChat file:InputEncryptedFile = EncryptedFile;
|
||||
|
||||
updates.getState#edd4882a = updates.State;
|
||||
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.deleteChannel#c0111fe3 channel:InputChannel = 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.updatePinnedMessage#a72ded52 flags:# silent:flags.0?true channel:InputChannel id:int = Updates;
|
||||
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.getLanguages#800fd57d = Vector<LangPackLanguage>;
|
||||
|
||||
// LAYER 73
|
||||
// LAYER 74
|
||||
|
|
|
@ -311,8 +311,10 @@ class TLGenerator:
|
|||
|
||||
for ra in repeated_args.values():
|
||||
if len(ra) > 1:
|
||||
cnd1 = ('self.{}'.format(a.name) for a in ra)
|
||||
cnd2 = ('not self.{}'.format(a.name) for a in ra)
|
||||
cnd1 = ('(self.{0} or self.{0} is not None)'
|
||||
.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(
|
||||
"assert ({}) or ({}), '{} parameters must all "
|
||||
"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,
|
||||
# so we need an extra join here. Note that empty vector flags
|
||||
# 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:
|
||||
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.use_vector_id:
|
||||
|
@ -495,11 +499,14 @@ class TLGenerator:
|
|||
# There's a flag indicator, but no flag arguments so it's 0
|
||||
builder.write(r"b'\0\0\0\0'")
|
||||
else:
|
||||
builder.write("struct.pack('<I', {})".format(
|
||||
' | '.join('({} if {} else 0)'.format(
|
||||
1 << flag.flag_index, 'self.{}'.format(flag.name)
|
||||
) for flag in args if flag.is_flag)
|
||||
))
|
||||
builder.write("struct.pack('<I', ")
|
||||
builder.write(
|
||||
' | '.join('(0 if {0} is None or {0} is False else {1})'
|
||||
.format('self.{}'.format(flag.name),
|
||||
1 << flag.flag_index)
|
||||
for flag in args if flag.is_flag)
|
||||
)
|
||||
builder.write(')')
|
||||
|
||||
elif 'int' == arg.type:
|
||||
# struct.pack is around 4 times faster than int.to_bytes
|
||||
|
@ -533,11 +540,7 @@ class TLGenerator:
|
|||
builder.write('TLObject.serialize_bytes({})'.format(name))
|
||||
|
||||
elif 'date' == arg.type: # Custom format
|
||||
# 0 if datetime is None else int(datetime.timestamp())
|
||||
builder.write(
|
||||
r"b'\0\0\0\0' if {0} is None else "
|
||||
r"struct.pack('<I', int({0}.timestamp()))".format(name)
|
||||
)
|
||||
builder.write('TLObject.serialize_datetime({})'.format(name))
|
||||
|
||||
else:
|
||||
# Else it may be a custom type
|
||||
|
|
|
@ -18,7 +18,7 @@ class HigherLevelTests(unittest.TestCase):
|
|||
@staticmethod
|
||||
def test_cdn_download():
|
||||
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()
|
||||
|
||||
try:
|
||||
|
|
Loading…
Reference in New Issue
Block a user