Merge branch 'master' of github.com:lonamiwebs/Telethon

This commit is contained in:
Tanuj 2017-12-25 15:40:25 +00:00
commit e4b471105a
69 changed files with 3378 additions and 650 deletions

5
.gitignore vendored
View File

@ -1,4 +1,7 @@
.idea
# Docs
_build/
# Generated code
telethon/tl/functions/
telethon/tl/types/
telethon/tl/all_tlobjects.py

20
readthedocs/Makefile Normal file
View File

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SPHINXPROJ = Telethon
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

174
readthedocs/conf.py Normal file
View File

@ -0,0 +1,174 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Telethon documentation build configuration file, created by
# sphinx-quickstart on Fri Nov 17 15:36:11 2017.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ['sphinx.ext.autodoc']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'Telethon'
copyright = '2017, Lonami'
author = 'Lonami'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '0.15'
# The full version, including alpha/beta/rc tags.
release = '0.15.5'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
html_theme_options = {
'collapse_navigation': True,
'display_version': True,
'navigation_depth': 3,
}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# This is required for the alabaster theme
# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
html_sidebars = {
'**': [
'globaltoc.html',
'relations.html', # needs 'show_related': True theme option to display
'searchbox.html',
]
}
# -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'Telethondoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'Telethon.tex', 'Telethon Documentation',
'Jeff', 'manual'),
]
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'telethon', 'Telethon Documentation',
[author], 1)
]
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'Telethon', 'Telethon Documentation',
author, 'Telethon', 'One line description of project.',
'Miscellaneous'),
]

View File

@ -0,0 +1,59 @@
======
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:
.. code-block:: python
from telethon.tl.functions.messages import GetInlineBotResultsRequest
bot_results = client(GetInlineBotResultsRequest(
bot, user_or_chat, 'query', ''
))
And you can select any of their results by using
`SendInlineBotResultRequest`__:
.. code-block:: python
from telethon.tl.functions.messages import SendInlineBotResultRequest
client(SendInlineBotResultRequest(
get_input_peer(user_or_chat),
obtained_query_id,
obtained_str_id
))
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`__:
.. code-block:: python
from telethon.tl.functions.messages import GetBotCallbackAnswerRequest
client(GetBotCallbackAnswerRequest(
user_or_chat,
msg.id,
data=msg.reply_markup.rows[wanted_row].buttons[wanted_button].data
))
Its a bit verbose, but it has all the information you would need to
show it visually (button rows, and buttons within each row, each with
its own data).
__ 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

View File

@ -0,0 +1,58 @@
=========================
Signing In
=========================
.. note::
Make sure you have gone through :ref:`prelude` already!
Two Factor Authorization (2FA)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
If you have Two Factor Authorization (from now on, 2FA) enabled on your account, calling
:meth:`telethon.TelegramClient.sign_in` will raise a `SessionPasswordNeededError`.
When this happens, just :meth:`telethon.TelegramClient.sign_in` again with a ``password=``:
.. code-block:: python
import getpass
from telethon.errors import SessionPasswordNeededError
client.sign_in(phone)
try:
client.sign_in(code=input('Enter code: '))
except SessionPasswordNeededError:
client.sign_in(password=getpass.getpass())
Enabling 2FA
*************
If you don't have 2FA enabled, but you would like to do so through Telethon, take as example the following code snippet:
.. code-block:: python
import os
from hashlib import sha256
from telethon.tl.functions import account
from telethon.tl.types.account import PasswordInputSettings
new_salt = client(account.GetPasswordRequest()).new_salt
salt = new_salt + os.urandom(8) # new random salt
pw = 'secret'.encode('utf-8') # type your new password here
hint = 'hint'
pw_salted = salt + pw + salt
pw_hash = sha256(pw_salted).digest()
result = client(account.UpdatePasswordSettingsRequest(
current_password_hash=salt,
new_settings=PasswordInputSettings(
new_salt=salt,
new_password_hash=pw_hash,
hint=hint
)
))
Thanks to `Issue 259 <https://github.com/LonamiWebs/Telethon/issues/259>`_ for the tip!

View File

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

View File

@ -0,0 +1,103 @@
=========================
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).
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
from telethon.tl.functions.messages import ForwardMessagesRequest
# note the s ^
messages = foo() # retrieve a few messages (or even one, in a list)
from_entity = bar()
to_entity = baz()
client(ForwardMessagesRequest(
from_peer=from_entity, # who sent these messages?
id=[msg.id for msg in messages], # which are the messages?
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.
Searching Messages
*******************
Messages are searched through the obvious SearchRequest_, but you may run into issues_. A valid example would be:
.. code-block:: python
result = client(SearchRequest(
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,
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 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``.
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:
.. code-block:: python
# Get all the sticker sets this user has
sticker_sets = client(GetAllStickersRequest(0))
# Choose a sticker set
sticker_set = sticker_sets.sets[0]
# Get the stickers for this sticker set
stickers = client(GetStickerSetRequest(
stickerset=InputStickerSetID(
id=sticker_set.id, access_hash=sticker_set.access_hash
)
))
# Stickers are nothing more than files, so send that
client(SendMediaRequest(
peer=client.get_me(),
media=InputMediaDocument(
id=InputDocument(
id=stickers.documents[0].id,
access_hash=stickers.documents[0].access_hash
),
caption=''
)
))
.. _ForwardMessageRequest: https://lonamiwebs.github.io/Telethon/methods/messages/forward_message.html
.. _ForwardMessagesRequest: https://lonamiwebs.github.io/Telethon/methods/messages/forward_messages.html
.. _SearchRequest: https://lonamiwebs.github.io/Telethon/methods/messages/search.html
.. _issues: https://github.com/LonamiWebs/Telethon/issues/215
.. _InputUserEmpty: https://lonamiwebs.github.io/Telethon/constructors/input_user_empty.html

View File

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

View File

@ -0,0 +1,117 @@
.. _accessing-the-full-api:
==========================
Accessing the Full API
==========================
The ``TelegramClient`` doesnt offer a method for every single request
the Telegram API supports. However, its very simple to ``.invoke()``
any request. Whenever you need something, dont 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.
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 thats the reason for this distinction.
Say ``client.send_message()`` didnt 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 wont be aware of this class! Now we have:
.. code-block:: python
from telethon.tl.functions.messages import SendMessageRequest
If youre going to use a lot of these, you may do:
.. code-block:: python
import telethon.tl.functions as tl
# We now have access to 'tl.messages.SendMessageRequest'
We see that this request must take at least two parameters, a ``peer``
of type `InputPeer`__, and a ``message`` which is just a Python
``str``\ ing.
How can we retrieve this ``InputPeer``? We have two options. We manually
`construct one`__, for instance:
.. code-block:: python
from telethon.tl.types import InputPeerUser
peer = InputPeerUser(user_id, user_hash)
Or we call ``.get_input_entity()``:
.. code-block:: python
peer = client.get_input_entity('someone')
When youre 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:
.. 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 doesnt have to do this
every time its used, simply call ``.get_input_peer``:
.. code-block:: python
from telethon import utils
peer = utils.get_input_user(entity)
After this small parenthesis about ``.get_entity`` versus
``.get_input_entity``, we have everything we need. To ``.invoke()`` our
request we do:
.. code-block:: python
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:
.. code-block:: python
result = client(SendMessageRequest(
client.get_input_entity('username'), 'Hello there!'
))
.. 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).
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.
__ https://lonamiwebs.github.io/Telethon
__ 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

View File

@ -0,0 +1,76 @@
.. _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.
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.
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:
.. 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)
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:
``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:
``client.is_user_authorized() # Returns True if you can send requests``
If you're not authorized, you need to ``.sign_in()``:
.. code-block:: python
client.send_code_request(phone_number)
myself = client.sign_in(phone_number, input('Enter code: '))
# If .sign_in raises PhoneNumberUnoccupiedError, use .sign_up instead
# 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!
.. note::
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
import socks
client = TelegramClient('session_id',
api_id=12345, api_hash='0123456789abcdef0123456789abcdef',
proxy=(socks.SOCKS5, 'localhost', 4444)
)
The ``proxy=`` argument should be a tuple, a list or a dict,
consisting of parameters described `here`__.
__ https://github.com/Anorov/PySocks#installation
__ https://github.com/Anorov/PySocks#usage-1%3E

View File

@ -0,0 +1,54 @@
.. Telethon documentation master file, created by
sphinx-quickstart on Fri Nov 17 15:36:11 2017.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
=================
Getting Started!
=================
Simple Installation
*********************
``pip install telethon``
**More details**: :ref:`installation`
Creating a client
**************
.. code-block:: python
from telethon import TelegramClient
# These example values won't work. You must get your own api_id and
# api_hash from https://my.telegram.org, under API Development.
api_id = 12345
api_hash = '0123456789abcdef0123456789abcdef'
phone = '+34600000000'
client = TelegramClient('session_name', api_id, api_hash)
client.connect()
# If you already have a previous 'session_name.session' file, skip this.
client.sign_in(phone=phone)
me = client.sign_in(code=77777) # Put whatever code you received here.
**More details**: :ref:`creating-a-client`
Simple Stuff
**************
.. code-block:: python
print(me.stringify())
client.send_message('username', 'Hello! Talking to you from Telethon')
client.send_file('username', '/home/myself/Pictures/holidays.jpg')
client.download_profile_photo(me)
total, messages, senders = client.get_message_history('username')
client.download_media(messages[0])

View File

@ -0,0 +1,71 @@
.. _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 already have the library installed, upgrade with:
``pip install --upgrade telethon``
You can also install the library directly from GitHub or a fork:
.. code-block:: python
# pip install git+https://github.com/LonamiWebs/Telethon.git
or
$ git clone https://github.com/LonamiWebs/Telethon.git
$ cd Telethon/
# pip install -Ue .
If you don't have root access, simply pass the ``--user`` flag to the pip command.
Manual Installation
^^^^^^^^^^^^^^^^^^^^
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``
3. Enter the cloned repository: ``cd Telethon``
4. Run the code generator: ``python3 setup.py gen_tl``
5. Done!
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!
__ 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

View File

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

View File

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

View File

@ -0,0 +1,135 @@
.. _working-with-updates:
====================
Working with Updates
====================
.. contents::
The library can run in four distinguishable modes:
- With no extra threads at all.
- With an extra thread that receives everything as soon as possible (default).
- 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.
Using multiple workers
^^^^^^^^^^^^^^^^^^^^^^^
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.
The next thing you want to do is to add a method that will be called when an `Update`__ arrives:
.. code-block:: python
def callback(update):
print('I received', update)
client.add_update_handler(callback)
# 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:
.. code-block:: python
from telethon.tl.types import UpdateShortMessage, PeerUser
def replier(update):
if isinstance(update, UpdateShortMessage) and not update.out:
client.send_message(PeerUser(update.user_id), update.message[::-1])
client.add_update_handler(replier)
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.
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:
.. code-block:: python
while True:
try:
update = client.updates.poll()
if not update:
continue
print('I received', update)
except KeyboardInterrupt:
break
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``.
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.
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:
.. code-block:: python
client = TelegramClient(
...
spawn_read_thread=False
)
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`__.
As a complete example:
.. code-block:: python
def callback(update):
print('I received', update)
client = TelegramClient('session', api_id, api_hash,
update_workers=1, spawn_read_thread=False)
client.connect()
client.add_update_handler(callback)
client.idle() # ends with Ctrl+C
client.disconnect()
__ 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

View File

@ -0,0 +1,26 @@
=========================================
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,
as they are often used to spam other accounts,
likely through the use of libraries like this one.
The best advice we can give you is to not abuse the API,
like calling many requests really quickly,
and to sign up with these phones through an official application.
Telegram may also ban virtual (VoIP) phone numbers,
as again, they're likely to be used for spam.
If you want to check if your account has been limited,
simply send a private message to `@SpamBot`__ through Telegram itself.
You should notice this by getting errors like ``PeerFloodError``,
which means you're limited, for instance,
when sending a message to some accounts but not others.
For more discussion, please see `issue 297`__.
__ https://t.me/SpamBot
__ https://github.com/LonamiWebs/Telethon/issues/297

View File

@ -0,0 +1,24 @@
================
Enable Logging
================
Telethon makes use of the `logging`__ module, and you can enable it as follows:
.. code-block:: python
import logging
logging.basicConfig(level=logging.DEBUG)
You can also use it in your own project very easily:
.. code-block:: python
import logging
logger = logging.getLogger(__name__)
logger.debug('Debug messages')
logger.info('Useful information')
logger.warning('This is a warning!')
__ https://docs.python.org/3/library/logging.html

View File

@ -0,0 +1,27 @@
==========
RPC Errors
==========
RPC stands for Remote Procedure Call, and when Telethon raises an
``RPCError``, its most likely because you have invoked some of the API
methods incorrectly (wrong parameters, wrong permissions, or even
something went wrong on Telegrams server). The most common are:
- ``FloodError`` (420), the same request was repeated many times. Must
wait ``.seconds``.
- ``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 dont have permissions to perform
said operation on a chat or channel. Try avoiding filters, i.e. when
searching messages.
The generic classes for different error codes are: \* ``InvalidDCError``
(303), the request must be repeated on another DC. \*
``BadRequestError`` (400), the request contained errors. \*
``UnauthorizedError`` (401), the user is not authorized yet. \*
``ForbiddenError`` (403), privacy violation error. \* ``NotFoundError``
(404), make sure youre invoking ``Request``\ s!
If the error is not recognised, it will only be an ``RPCError``.

65
readthedocs/index.rst Normal file
View File

@ -0,0 +1,65 @@
.. Telethon documentation master file, created by
sphinx-quickstart on Fri Nov 17 15:36:11 2017.
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>`_.
.. _installation-and-usage:
.. toctree::
:maxdepth: 2
:caption: Installation and Simple Usage
extra/basic/getting-started
extra/basic/installation
extra/basic/creating-a-client
extra/basic/sessions
extra/basic/sending-requests
extra/basic/working-with-updates
extra/basic/accessing-the-full-api
.. _Advanced-usage:
.. toctree::
: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
.. _Troubleshooting:
.. toctree::
:maxdepth: 2
:caption: Troubleshooting
extra/troubleshooting/enable-logging
extra/troubleshooting/deleted-limited-or-deactivated-accounts
extra/troubleshooting/rpc-errors
.. toctree::
:caption: Telethon modules
telethon
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

36
readthedocs/make.bat Normal file
View File

@ -0,0 +1,36 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
set SPHINXPROJ=Telethon
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
:end
popd

7
readthedocs/modules.rst Normal file
View File

@ -0,0 +1,7 @@
telethon
========
.. toctree::
:maxdepth: 3
telethon

View File

@ -0,0 +1 @@
telethon

View File

@ -0,0 +1,61 @@
telethon\.crypto package
========================
telethon\.crypto\.aes module
----------------------------
.. automodule:: telethon.crypto.aes
:members:
:undoc-members:
:show-inheritance:
telethon\.crypto\.aes\_ctr module
---------------------------------
.. automodule:: telethon.crypto.aes_ctr
:members:
:undoc-members:
:show-inheritance:
telethon\.crypto\.auth\_key module
----------------------------------
.. automodule:: telethon.crypto.auth_key
:members:
:undoc-members:
:show-inheritance:
telethon\.crypto\.cdn\_decrypter module
---------------------------------------
.. automodule:: telethon.crypto.cdn_decrypter
:members:
:undoc-members:
:show-inheritance:
telethon\.crypto\.factorization module
--------------------------------------
.. automodule:: telethon.crypto.factorization
:members:
:undoc-members:
:show-inheritance:
telethon\.crypto\.libssl module
-------------------------------
.. automodule:: telethon.crypto.libssl
:members:
:undoc-members:
:show-inheritance:
telethon\.crypto\.rsa module
----------------------------
.. automodule:: telethon.crypto.rsa
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,21 @@
telethon\.errors package
========================
telethon\.errors\.common module
-------------------------------
.. automodule:: telethon.errors.common
:members:
:undoc-members:
:show-inheritance:
telethon\.errors\.rpc\_base\_errors module
------------------------------------------
.. automodule:: telethon.errors.rpc_base_errors
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,29 @@
telethon\.extensions package
============================
telethon\.extensions\.binary\_reader module
-------------------------------------------
.. automodule:: telethon.extensions.binary_reader
:members:
:undoc-members:
:show-inheritance:
telethon\.extensions\.markdown module
-------------------------------------
.. automodule:: telethon.extensions.markdown
:members:
:undoc-members:
:show-inheritance:
telethon\.extensions\.tcp\_client module
----------------------------------------
.. automodule:: telethon.extensions.tcp_client
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,37 @@
telethon\.network package
=========================
telethon\.network\.authenticator module
---------------------------------------
.. automodule:: telethon.network.authenticator
:members:
:undoc-members:
:show-inheritance:
telethon\.network\.connection module
------------------------------------
.. automodule:: telethon.network.connection
:members:
:undoc-members:
:show-inheritance:
telethon\.network\.mtproto\_plain\_sender module
------------------------------------------------
.. automodule:: telethon.network.mtproto_plain_sender
:members:
:undoc-members:
:show-inheritance:
telethon\.network\.mtproto\_sender module
-----------------------------------------
.. automodule:: telethon.network.mtproto_sender
:members:
:undoc-members:
:show-inheritance:

89
readthedocs/telethon.rst Normal file
View File

@ -0,0 +1,89 @@
telethon package
================
telethon\.helpers module
------------------------
.. automodule:: telethon.helpers
:members:
:undoc-members:
:show-inheritance:
telethon\.telegram\_bare\_client module
---------------------------------------
.. automodule:: telethon.telegram_bare_client
:members:
:undoc-members:
:show-inheritance:
telethon\.telegram\_client module
---------------------------------
.. automodule:: telethon.telegram_client
:members:
:undoc-members:
:show-inheritance:
telethon\.update\_state module
------------------------------
.. automodule:: telethon.update_state
:members:
:undoc-members:
:show-inheritance:
telethon\.utils module
----------------------
.. automodule:: telethon.utils
:members:
:undoc-members:
:show-inheritance:
telethon\.cryto package
------------------------
.. toctree::
telethon.crypto
telethon\.errors package
------------------------
.. toctree::
telethon.errors
telethon\.extensions package
------------------------
.. toctree::
telethon.extensions
telethon\.network package
------------------------
.. toctree::
telethon.network
telethon\.tl package
------------------------
.. toctree::
telethon.tl
Module contents
---------------
.. automodule:: telethon
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,12 @@
telethon\.tl\.custom package
============================
telethon\.tl\.custom\.draft module
----------------------------------
.. automodule:: telethon.tl.custom.draft
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,57 @@
telethon\.tl package
====================
.. toctree::
telethon.tl.custom
telethon\.tl\.entity\_database module
-------------------------------------
.. automodule:: telethon.tl.entity_database
:members:
:undoc-members:
:show-inheritance:
telethon\.tl\.gzip\_packed module
---------------------------------
.. automodule:: telethon.tl.gzip_packed
:members:
:undoc-members:
:show-inheritance:
telethon\.tl\.message\_container module
---------------------------------------
.. automodule:: telethon.tl.message_container
:members:
:undoc-members:
:show-inheritance:
telethon\.tl\.session module
----------------------------
.. automodule:: telethon.tl.session
:members:
:undoc-members:
:show-inheritance:
telethon\.tl\.tl\_message module
--------------------------------
.. automodule:: telethon.tl.tl_message
:members:
:undoc-members:
:show-inheritance:
telethon\.tl\.tlobject module
-----------------------------
.. automodule:: telethon.tl.tlobject
:members:
:undoc-members:
:show-inheritance:

View File

@ -15,16 +15,11 @@ Extra supported commands are:
from codecs import open
from sys import argv
import os
import re
# Always prefer setuptools over distutils
from setuptools import find_packages, setup
try:
from telethon import TelegramClient
except Exception as e:
print('Failed to import TelegramClient due to', e)
TelegramClient = None
class TempWorkDir:
"""Switches the working directory to be the one on which this file lives,
@ -76,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
@ -94,21 +99,15 @@ def main():
fetch_errors(ERRORS_JSON)
else:
if not TelegramClient:
gen_tl()
from telethon import TelegramClient as TgClient
version = TgClient.__version__
else:
version = TelegramClient.__version__
# Get the long description from the README file
with open('README.rst', encoding='utf-8') as f:
long_description = f.read()
with open('telethon/version.py', encoding='utf-8') as f:
version = re.search(r"^__version__\s+=\s+'(.*)'$",
f.read(), flags=re.MULTILINE).group(1)
setup(
name='Telethon',
# Versions should comply with PEP440.
version=version,
description="Full-featured Telegram client library for Python 3",
long_description=long_description,

View File

@ -1,4 +1,9 @@
import logging
from .telegram_bare_client import TelegramBareClient
from .telegram_client import TelegramClient
from .network import ConnectionMode
from . import tl
from . import tl, version
__version__ = version.__version__
logging.getLogger(__name__).addHandler(logging.NullHandler())

View File

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

View File

@ -1,3 +1,6 @@
"""
AES IGE implementation in Python. This module may use libssl if available.
"""
import os
import 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

View File

@ -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)

View File

@ -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)

View File

@ -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
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
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()

View File

@ -1,71 +1,64 @@
"""
This module holds a fast Factorization class.
"""
from random import randint
try:
import sympy.ntheory
except ImportError:
sympy = None
class Factorization:
@staticmethod
def find_small_multiplier_lopatin(what):
"""Finds the small multiplier by using Lopatin's method"""
g = 0
for i in range(3):
q = (randint(0, 127) & 15) + 17
x = randint(0, 1000000000) + 1
y = x
lim = 1 << (i + 18)
for j in range(1, lim):
a, b, c = x, x, q
while b != 0:
if (b & 1) != 0:
c += a
if c >= what:
c -= what
a += a
if a >= what:
a -= what
b >>= 1
"""
Simple module to factorize large numbers really quickly.
"""
@classmethod
def factorize(cls, pq):
"""
Factorizes the given large integer.
x = c
z = y - x if x < y else x - y
g = Factorization.gcd(z, what)
if g != 1:
:param pq: the prime pair pq.
:return: a tuple containing the two factors p and q.
"""
if pq % 2 == 0:
return 2, pq // 2
y, c, m = randint(1, pq - 1), randint(1, pq - 1), randint(1, pq - 1)
g = r = q = 1
x = ys = 0
while g == 1:
x = y
for i in range(r):
y = (pow(y, 2, pq) + c) % pq
k = 0
while k < r and g == 1:
ys = y
for i in range(min(m, r - k)):
y = (pow(y, 2, pq) + c) % pq
q = q * (abs(x - y)) % pq
g = cls.gcd(q, pq)
k += m
r *= 2
if g == pq:
while True:
ys = (pow(ys, 2, pq) + c) % pq
g = cls.gcd(abs(x - ys), pq)
if g > 1:
break
if (j & (j - 1)) == 0:
y = x
if g > 1:
break
p = what // g
return min(p, g)
return g, pq // g
@staticmethod
def gcd(a, b):
"""Calculates the greatest common divisor"""
while a != 0 and b != 0:
while b & 1 == 0:
b >>= 1
while a & 1 == 0:
a >>= 1
if a > b:
a -= b
else:
b -= a
return a if b == 0 else b
@staticmethod
def factorize(pq):
"""Factorizes the given number and returns both
the divisor and the number divided by the divisor
"""
if sympy:
return tuple(sympy.ntheory.factorint(pq).keys())
else:
divisor = Factorization.find_small_multiplier_lopatin(pq)
return divisor, pq // divisor
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
return a

View File

@ -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

View File

@ -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)

View File

@ -1,3 +1,7 @@
"""
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
@ -13,6 +17,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 +41,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,

View File

@ -2,20 +2,23 @@
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"""
"""
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 +30,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 +46,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 +57,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 +67,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.'

View File

@ -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):

View File

@ -1,3 +1,6 @@
"""
This module contains the BinaryReader utility class.
"""
import os
from datetime import datetime
from io import BufferedReader, BytesIO
@ -30,32 +33,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 +70,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 +78,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,11 +99,11 @@ 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
@ -110,13 +114,13 @@ class BinaryReader:
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.fromtimestamp(value)
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,7 +139,7 @@ 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')
@ -145,21 +149,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)

View File

@ -0,0 +1,208 @@
"""
Simple markdown parser which does not support nesting. Intended primarily
for use within the library, which attempts to handle emojies correctly,
since they seem to count as two characters and it's a bit strange.
"""
import re
from ..tl import TLObject
from ..tl.types import (
MessageEntityBold, MessageEntityItalic, MessageEntityCode,
MessageEntityPre, MessageEntityTextUrl
)
DEFAULT_DELIMITERS = {
'**': MessageEntityBold,
'__': MessageEntityItalic,
'`': MessageEntityCode,
'```': MessageEntityPre
}
# Regex used to match utf-16le encoded r'\[(.+?)\]\((.+?)\)',
# reason why there's '\0' after every match-literal character.
DEFAULT_URL_RE = re.compile(b'\\[\0(.+?)\\]\0\\(\0(.+?)\\)\0')
# Reverse operation for DEFAULT_URL_RE. {0} for text, {1} for URL.
DEFAULT_URL_FORMAT = '[{0}]({1})'
# Encoding to be used
ENC = 'utf-16le'
def parse(message, delimiters=None, url_re=None):
"""
Parses the given markdown message and returns its stripped representation
plus a list of the MessageEntity's that were found.
: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
elif url_re:
if isinstance(url_re, bytes):
url_re = re.compile(url_re)
if not delimiters:
if delimiters is not None:
return message, []
delimiters = DEFAULT_DELIMITERS
delimiters = {k.encode(ENC): v for k, v in delimiters.items()}
# Cannot use a for loop because we need to skip some indices
i = 0
result = []
current = None
end_delimiter = None
# Work on byte level with the utf-16le encoding to get the offsets right.
# The offset will just be half the index we're at.
message = message.encode(ENC)
while i < len(message):
if url_re and current is None:
# If we're not inside a previous match since Telegram doesn't allow
# nested message entities, try matching the URL from the i'th pos.
url_match = url_re.match(message, pos=i)
if url_match:
# Replace the whole match with only the inline URL text.
message = b''.join((
message[:url_match.start()],
url_match.group(1),
message[url_match.end():]
))
result.append(MessageEntityTextUrl(
offset=i // 2, length=len(url_match.group(1)) // 2,
url=url_match.group(2).decode(ENC)
))
i += len(url_match.group(1))
# Next loop iteration, don't check delimiters, since
# a new inline URL might be right after this one.
continue
if end_delimiter is None:
# We're not expecting any delimiter, so check them all
for d, m in delimiters.items():
# Slice the string at the current i'th position to see if
# it matches the current delimiter d, otherwise skip it.
if message[i:i + len(d)] != d:
continue
if message[i + len(d):i + 2 * len(d)] == d:
# The same delimiter can't be right afterwards, if
# this were the case we would match empty strings
# like `` which we don't want to.
continue
# Get rid of the delimiter by slicing it away
message = message[:i] + message[i + len(d):]
if m == MessageEntityPre:
# Special case, also has 'lang'
current = m(i // 2, None, '')
else:
current = m(i // 2, None)
end_delimiter = d # We expect the same delimiter.
break
elif message[i:i + len(end_delimiter)] == end_delimiter:
message = message[:i] + message[i + len(end_delimiter):]
current.length = (i // 2) - current.offset
result.append(current)
current, end_delimiter = None, None
# Don't increment i here as we matched a delimiter,
# and there may be a new one right after. This is
# different than when encountering the first delimiter,
# as we already know there won't be the same right after.
continue
# Next iteration, utf-16 encoded characters need 2 bytes.
i += 2
# We may have found some a delimiter but not its ending pair.
# If this is the case, we want to insert the delimiter character back.
if current is not None:
message = (
message[:2 * current.offset]
+ end_delimiter
+ message[2 * current.offset:]
)
return message.decode(ENC), result
def unparse(text, entities, delimiters=None, url_fmt=None):
"""
Performs the reverse operation to .parse(), effectively returning
markdown-like syntax given a normal text and its MessageEntity's.
:param text: the text to be reconverted into markdown.
:param entities: the MessageEntity's applied to the text.
:return: a markdown-like text representing the combination of both inputs.
"""
if not delimiters:
if delimiters is not None:
return text
delimiters = DEFAULT_DELIMITERS
if url_fmt is None:
url_fmt = DEFAULT_URL_FORMAT
if isinstance(entities, TLObject):
entities = (entities,)
else:
entities = tuple(sorted(entities, key=lambda e: e.offset, reverse=True))
# Reverse the delimiters, and encode them as utf16
delimiters = {v: k.encode(ENC) for k, v in delimiters.items()}
text = text.encode(ENC)
for entity in entities:
s = entity.offset * 2
e = (entity.offset + entity.length) * 2
delimiter = delimiters.get(type(entity), None)
if delimiter:
text = text[:s] + delimiter + text[s:e] + delimiter + text[e:]
elif isinstance(entity, MessageEntityTextUrl) and url_fmt:
# If byte-strings supported .format(), we, could have converted
# the str url_fmt to a byte-string with the following regex:
# re.sub(b'{\0\s*(?:([01])\0)?\s*}\0',rb'{\1}',url_fmt.encode(ENC))
#
# This would preserve {}, {0} and {1}.
# Alternatively (as it's done), we can decode/encode it every time.
text = (
text[:s] +
url_fmt.format(text[s:e].decode(ENC), entity.url).encode(ENC) +
text[e:]
)
return text.decode(ENC)
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!'.
: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 not isinstance(entity, TLObject) and hasattr(entity, '__iter__'):
multiple = True
else:
entity = [entity]
multiple = False
text = text.encode(ENC)
result = []
for e in entity:
start = e.offset * 2
end = (e.offset + e.length) * 2
result.append(text[start:end].decode(ENC))
return result if multiple else result[0]

View File

@ -1,4 +1,6 @@
# Python rough implementation of a C# TCP client
"""
This module holds a rough implementation of the C# TCP client.
"""
import errno
import socket
from datetime import timedelta
@ -7,7 +9,14 @@ from threading import Lock
class TcpClient:
"""A simple TCP client to ease the work with sockets and proxies."""
def __init__(self, proxy=None, timeout=timedelta(seconds=5)):
"""
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._closing_lock = Lock()
@ -33,10 +42,19 @@ class TcpClient:
self._socket.settimeout(self.timeout)
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
if not ip.startswith('['):
ip = '[' + ip
if not ip.endswith(']'):
ip = ip + ']'
mode, address = socket.AF_INET6, (ip, port, 0, 0)
else:
mode, address = socket.AF_INET, (ip, port)
@ -59,12 +77,13 @@ class TcpClient:
raise
def _get_connected(self):
return self._socket is not None
"""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."""
if self._closing_lock.locked():
# Already closing, no need to close again (avoid None.close())
return
@ -80,7 +99,11 @@ class TcpClient:
self._socket = None
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:
raise ConnectionResetError()
@ -90,7 +113,7 @@ class TcpClient:
self._socket.sendall(data)
except socket.timeout as e:
raise TimeoutError() from e
except BrokenPipeError:
except ConnectionError:
self._raise_connection_reset()
except OSError as e:
if e.errno == errno.EBADF:
@ -99,13 +122,11 @@ class TcpClient:
raise
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.
A timeout can be specified, which will cancel the operation if
no data has been read in the specified time. If data was read
and it's waiting for more, the timeout will NOT cancel the
operation. Set to None for no timeout
:param size: the size of the block to be read.
:return: the read data with len(data) == size.
"""
if self._socket is None:
raise ConnectionResetError()
@ -118,6 +139,8 @@ class TcpClient:
partial = self._socket.recv(bytes_left)
except socket.timeout as e:
raise TimeoutError() from e
except ConnectionError:
self._raise_connection_reset()
except OSError as e:
if e.errno == errno.EBADF or e.errno == errno.ENOTSOCK:
self._raise_connection_reset()
@ -135,5 +158,6 @@ 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.')

View File

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

View File

@ -1,3 +1,7 @@
"""
This module contains several functions that authenticate the client machine
with Telegram's servers, effectively creating an authorization key.
"""
import os
import time
from hashlib import sha1
@ -18,6 +22,14 @@ from ..tl.functions import (
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 @@ def do_authentication(connection, retries=5):
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)
@ -113,6 +127,10 @@ def _do_authentication(connection):
key, iv = utils.generate_key_data_from_nonce(
res_pq.server_nonce, new_nonce
)
if len(server_dh_params.encrypted_answer) % 16 != 0:
# See PR#453
raise SecurityError('AES block size mismatch')
plain_text_answer = AES.decrypt_ige(
server_dh_params.encrypted_answer, key, iv
)
@ -191,8 +209,12 @@ 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)

View File

@ -1,3 +1,7 @@
"""
This module holds both the Connection class and the ConnectionMode enum,
which specifies the protocol to be used by the Connection.
"""
import os
import struct
from datetime import timedelta
@ -35,16 +39,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)):
"""
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
@ -75,6 +87,12 @@ class Connection:
setattr(self, 'read', self._read_plain)
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:
self.conn.connect(ip, port)
except OSError as e:
@ -92,9 +110,13 @@ class Connection:
self._setup_obfuscation()
def get_timeout(self):
"""Returns the timeout used by the connection."""
return self.conn.timeout
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:
@ -122,13 +144,19 @@ class Connection:
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
)
@ -141,6 +169,15 @@ class Connection:
raise ValueError('Invalid connection mode specified: ' + str(self._mode))
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.
"""
packet_len_seq = self.read(8) # 4 and 4
packet_len, seq = struct.unpack('<ii', packet_len_seq)
@ -154,9 +191,21 @@ class Connection:
return body
def _recv_intermediate(self):
"""
Receives a message from the network,
internally encoded using the TCP intermediate protocol.
:return: the read message payload.
"""
return self.read(struct.unpack('<i', self.read(4))[0])
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', self.read(1))[0]
if length >= 127:
length = struct.unpack('<i', self.read(3) + b'\0')[0]
@ -173,6 +222,12 @@ class Connection:
raise ValueError('Invalid connection mode specified: ' + str(self._mode))
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
@ -182,9 +237,21 @@ class Connection:
self.write(data + crc)
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.
"""
self.write(struct.pack('<i', len(message)) + message)
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)
@ -201,9 +268,21 @@ class Connection:
raise ValueError('Invalid connection mode specified: ' + str(self._mode))
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 self.conn.read(length)
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(
self.conn.read(length)
)
@ -216,9 +295,20 @@ class Connection:
raise ValueError('Invalid connection mode specified: ' + str(self._mode))
def _write_plain(self, data):
"""
Writes the given data through the socket connection.
:param data: the data in bytes to be written.
"""
self.conn.write(data)
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.
"""
self.conn.write(self._aes_encrypt.encrypt(data))
# endregion

View File

@ -1,3 +1,7 @@
"""
This module contains the class used to communicate with Telegram's servers
in plain text, when no authorization key has been created yet.
"""
import struct
import 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
def connect(self):
"""Connects to Telegram's servers."""
self._connection.connect()
def disconnect(self):
"""Disconnects from Telegram's servers."""
self._connection.close()
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.
"""
self._connection.send(
struct.pack('<QQi', 0, self._get_new_msg_id(), len(data)) + data
)
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 = 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)

View File

@ -1,3 +1,7 @@
"""
This module contains the class used to communicate with Telegram's servers
encrypting every packet, and relies on a valid AuthKey in the used Session.
"""
import gzip
import logging
import struct
@ -17,7 +21,7 @@ from ..tl.types import (
)
from ..tl.functions.auth import LogOutRequest
logging.getLogger(__name__).addHandler(logging.NullHandler())
__log__ = logging.getLogger(__name__)
class MtProtoSender:
@ -31,12 +35,17 @@ class MtProtoSender:
"""
def __init__(self, session, connection):
"""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.
"""
self.session = session
self.connection = connection
self._logger = logging.getLogger(__name__)
# Message IDs that need confirmation
self._need_confirmation = set()
@ -45,28 +54,36 @@ class MtProtoSender:
self._pending_receive = {}
def connect(self):
"""Connects to the server"""
"""Connects to the server."""
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._need_confirmation.clear()
self._clear_all_pending()
def clone(self):
"""Creates a copy of this MtProtoSender as a new connection"""
"""Creates a copy of this MtProtoSender as a new connection."""
return MtProtoSender(self.session, self.connection.clone())
# region Send and receive
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.
"""
# Finally send our packed request(s)
messages = [TLMessage(self.session, r) for r in requests]
self._pending_receive.update({m.msg_id: m for m in messages})
@ -91,18 +108,23 @@ class MtProtoSender:
self._send_message(message)
def _send_acknowledge(self, msg_id):
"""Sends a message acknowledge for the given msg_id"""
"""Sends a message acknowledge for the given msg_id."""
self._send_message(TLMessage(self.session, MsgsAck([msg_id])))
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 = self.connection.recv()
@ -114,6 +136,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 +151,13 @@ class MtProtoSender:
# region Low level processing
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 +169,12 @@ class MtProtoSender:
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,101 +205,114 @@ class MtProtoSender:
return message, remote_msg_id, remote_sequence
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
self._need_confirmation.add(msg_id)
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 self._handle_rpc_result(msg_id, sequence, reader)
if code == Pong.CONSTRUCTOR_ID:
return self._handle_pong(msg_id, sequence, reader)
if code == MessageContainer.CONSTRUCTOR_ID:
__log__.debug('Processing container result')
return self._handle_container(msg_id, sequence, reader, state)
if code == GzipPacked.CONSTRUCTOR_ID:
__log__.debug('Processing gzipped result')
return self._handle_gzip_packed(msg_id, sequence, reader, state)
if code == BadServerSalt.CONSTRUCTOR_ID:
return 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 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 self._handle_msg_detailed_info(msg_id, sequence, reader)
if isinstance(obj, Pong):
return self._handle_pong(msg_id, sequence, obj)
if code == MsgNewDetailedInfo.CONSTRUCTOR_ID:
return self._handle_msg_new_detailed_info(msg_id, sequence, reader)
if isinstance(obj, BadServerSalt):
return self._handle_bad_server_salt(msg_id, sequence, obj)
if code == NewSessionCreated.CONSTRUCTOR_ID:
return self._handle_new_session_created(msg_id, sequence, reader)
if isinstance(obj, BadMsgNotification):
return 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)
if isinstance(obj, MsgDetailedInfo):
return self._handle_msg_detailed_info(msg_id, sequence, obj)
if isinstance(obj, MsgNewDetailedInfo):
return self._handle_msg_new_detailed_info(msg_id, sequence, obj)
if isinstance(obj, NewSessionCreated):
return 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]
@ -277,13 +323,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()
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:
@ -292,21 +344,31 @@ class MtProtoSender:
if requests:
return self.send(*requests)
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
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()
@ -322,15 +384,16 @@ class MtProtoSender:
return True
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)
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
@ -338,60 +401,91 @@ class MtProtoSender:
self._resend_request(bad_salt.bad_msg_id)
return True
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)
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')
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')
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')
self._resend_request(bad_msg.bad_msg_id)
return True
else:
raise error
def _handle_msg_detailed_info(self, msg_id, sequence, reader):
msg_new = reader.tgread_object()
assert isinstance(msg_new, MsgDetailedInfo)
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
self._send_acknowledge(msg_new.answer_msg_id)
return True
def _handle_msg_new_detailed_info(self, msg_id, sequence, reader):
msg_new = reader.tgread_object()
assert isinstance(msg_new, MsgNewDetailedInfo)
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
self._send_acknowledge(msg_new.answer_msg_id)
return True
def _handle_new_session_created(self, msg_id, sequence, reader):
new_session = reader.tgread_object()
assert isinstance(new_session, NewSessionCreated)
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
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 +511,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 +528,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
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:
# We are reentering process_msg, which seemingly the same msg_id
# to the self._need_confirmation set. Remove it from there first

View File

@ -9,7 +9,7 @@ from signal import signal, SIGINT, SIGTERM, SIGABRT
from threading import Lock
from time import sleep
from . import helpers as utils
from . import helpers as utils, version
from .crypto import rsa, CdnDecrypter
from .errors import (
RPCError, BrokenAuthKeyError, ServerError,
@ -39,6 +39,13 @@ from .update_state import UpdateState
from .utils import get_appropriated_part_size
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 -
@ -60,7 +67,7 @@ class TelegramBareClient:
"""
# Current TelegramClient version
__version__ = '0.15.3'
__version__ = version.__version__
# TODO Make this thread-safe, all connections share the same DC
_config = None # Server configuration (with .dc_options)
@ -69,6 +76,7 @@ class TelegramBareClient:
def __init__(self, session, api_id, api_hash,
connection_mode=ConnectionMode.TCP_FULL,
use_ipv6=False,
proxy=None,
update_workers=None,
spawn_read_thread=False,
@ -80,6 +88,8 @@ class TelegramBareClient:
"Your API ID or Hash cannot be empty or None. "
"Refer to Telethon's README.rst 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)
@ -88,6 +98,13 @@ class TelegramBareClient:
'The given session must be a str or a Session instance.'
)
# ':' 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
self.session = session
self.api_id = int(api_id)
self.api_hash = api_hash
@ -102,8 +119,6 @@ class TelegramBareClient:
mode=connection_mode, proxy=proxy, timeout=timeout
))
self._logger = logging.getLogger(__name__)
# Two threads may be calling reconnect() when the connection is lost,
# we only want one to actually perform the reconnection.
self._reconnect_lock = Lock()
@ -176,11 +191,15 @@ 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)
self._main_thread_ident = threading.get_ident()
self._background_error = None # Clear previous errors
try:
self._sender.connect()
__log__.info('Connection success!')
# Connection was successful! Try syncing the update state
# UNLESS '_sync_updates' is False (we probably are in
@ -200,16 +219,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 self.connect(_sync_updates=_sync_updates)
except (RPCError, ConnectionError) as error:
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.debug(
'Could not stabilise initial connection: {}'.format(error)
)
return False
def is_connected(self):
@ -231,14 +249,19 @@ class TelegramBareClient:
def disconnect(self):
"""Disconnects from the Telegram server
and stops all the spawned threads"""
__log__.info('Disconnecting...')
self._user_connected = False # This will stop recv_thread's loop
__log__.debug('Stopping all workers...')
self.updates.stop_workers()
# This will trigger a "ConnectionResetError" on the recv_thread,
# which won't attempt reconnecting as ._user_connected is False.
__log__.debug('Disconnecting the socket...')
self._sender.disconnect()
if self._recv_thread:
__log__.debug('Joining the read thread...')
self._recv_thread.join()
# TODO Shall we clear the _exported_sessions, or may be reused?
@ -254,21 +277,22 @@ class TelegramBareClient:
connects to the new data center.
"""
if new_dc is None:
# Assume we are disconnected due to some error, so connect again
with self._reconnect_lock:
# Another thread may have connected again, so check that first
if self.is_connected():
return True
if self.is_connected():
__log__.info('Reconnection aborted: already connected')
return True
try:
return self.connect()
except ConnectionResetError:
return False
try:
__log__.info('Attempting reconnection...')
return self.connect()
except ConnectionResetError as e:
__log__.warning('Reconnection failed due to %s', e)
return False
else:
# Since we're reconnecting possibly due to a UserMigrateError,
# we need to first know the Data Centers we can connect to. Do
# that before disconnecting.
dc = 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
@ -287,7 +311,7 @@ class TelegramBareClient:
return self._recv_thread is not None and \
threading.get_ident() == self._recv_thread.ident
def _get_dc(self, dc_id, ipv6=False, cdn=False):
def _get_dc(self, dc_id, cdn=False):
"""Gets the Data Center (DC) associated to 'dc_id'"""
if not TelegramBareClient._config:
TelegramBareClient._config = self(GetConfigRequest())
@ -300,7 +324,7 @@ class TelegramBareClient:
return next(
dc for dc in TelegramBareClient._config.dc_options
if dc.id == dc_id and bool(dc.ipv6) == ipv6 and bool(dc.cdn) == cdn
if dc.id == dc_id and bool(dc.ipv6) == self._use_ipv6 and bool(dc.cdn) == cdn
)
except StopIteration:
if not cdn:
@ -308,7 +332,7 @@ class TelegramBareClient:
# New configuration, perhaps a new CDN was added?
TelegramBareClient._config = self(GetConfigRequest())
return self._get_dc(dc_id, ipv6=ipv6, cdn=cdn)
return self._get_dc(dc_id, cdn=cdn)
def _get_exported_client(self, dc_id):
"""Creates and connects a new TelegramBareClient for the desired DC.
@ -330,6 +354,7 @@ class TelegramBareClient:
dc = self._get_dc(dc_id)
# Export the current authorization to the new DC.
__log__.info('Exporting authorization for data center %s', dc)
export_auth = self(ExportAuthorizationRequest(dc_id))
# Create a temporary session for this IP address, which needs
@ -342,6 +367,7 @@ class TelegramBareClient:
session.port = 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,
@ -353,7 +379,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
@ -368,6 +394,7 @@ class TelegramBareClient:
session.port = 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,
@ -397,12 +424,23 @@ class TelegramBareClient:
x.content_related for x in requests):
raise ValueError('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])
# Determine the sender to be used (main or a new connection)
on_main_thread = threading.get_ident() == self._main_thread_ident
if on_main_thread or self._on_read_thread():
__log__.debug('Invoking %s from main thread', which)
sender = self._sender
update_state = self.updates
else:
__log__.debug('Invoking %s from background thread. '
'Creating temporary connection', which)
sender = self._sender.clone()
sender.connect()
# We're on another connection, Telegram will resend all the
@ -421,7 +459,7 @@ class TelegramBareClient:
call_receive = not on_main_thread or self._recv_thread is None \
or self._reconnect_lock.locked()
try:
for _ in range(retries):
for attempt in range(retries):
if self._background_error and on_main_thread:
raise self._background_error
@ -431,6 +469,20 @@ 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], attempt + 1)
sleep(1)
# The ReadThread has priority when attempting reconnection,
# since this thread is constantly running while __call__ is
# only done sometimes. Here try connecting only once/retry.
if sender == self._sender:
if not self._reconnect_lock.locked():
with self._reconnect_lock:
self._reconnect()
else:
sender.connect()
raise ValueError('Number of retries reached 0.')
finally:
if sender != self._sender:
@ -453,11 +505,13 @@ class TelegramBareClient:
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.session.auth_key, self.session.time_offset = \
authenticator.do_authentication(self._sender.connection)
init_connection = True
if init_connection:
__log__.info('Initializing a new connection while invoking')
if len(requests) == 1:
requests = [self._wrap_init_connection(requests[0])]
else:
@ -484,28 +538,20 @@ class TelegramBareClient:
sender.receive(update_state=update_state)
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:
if not self._user_connected or self._reconnect_lock.locked():
# Only attempt reconnecting if the user called connect and not
# reconnecting already.
raise
self._logger.debug('Server disconnected us. Reconnecting and '
'resending request...')
if sender != self._sender:
# TODO Try reconnecting forever too?
sender.connect()
__log__.warning('Connection was reset while invoking')
if self._user_connected:
# Server disconnected us, __call__ will try reconnecting.
return None
else:
while self._user_connected and not self._reconnect():
sleep(0.1) # Retry forever until we can send the request
return None
# User never called .connect(), so raise this error.
raise
if init_connection:
# We initialized the connection successfully, even if
@ -528,10 +574,6 @@ 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)
)
# TODO What happens with the background thread here?
# For normal use cases, this won't happen, because this will only
@ -542,17 +584,13 @@ class TelegramBareClient:
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
)
sleep(e.seconds)
# Some really basic functionality
@ -615,6 +653,8 @@ class TelegramBareClient:
file_id = utils.generate_random_long()
hash_md5 = md5()
__log__.info('Uploading file of %d bytes in %d chunks of %d',
file_size, part_count, part_size)
stream = open(file, 'rb') if isinstance(file, str) else BytesIO(file)
try:
for part_index in range(part_count):
@ -631,6 +671,7 @@ class TelegramBareClient:
result = self(request)
if result:
__log__.debug('Uploaded %d/%d', part_index, part_count)
if not is_large:
# No need to update the hash if it's a large file
hash_md5.update(part)
@ -699,6 +740,7 @@ class TelegramBareClient:
client = self
cdn_decrypter = None
__log__.info('Downloading file in chunks of %d bytes', part_size)
try:
offset = 0
while True:
@ -711,12 +753,14 @@ class TelegramBareClient:
))
if isinstance(result, FileCdnRedirect):
__log__.info('File lives in a CDN')
cdn_decrypter, result = \
CdnDecrypter.prepare_decrypter(
client, self._get_cdn_client(result), result
)
except FileMigrateError as e:
__log__.info('File lives in another DC')
client = self._get_exported_client(e.new_dc)
continue
@ -729,6 +773,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:
@ -790,7 +835,6 @@ class TelegramBareClient:
if self._user_connected:
self.disconnect()
else:
self._logger.debug('Forcing exit...')
os._exit(1)
def idle(self, stop_signals=(SIGINT, SIGTERM, SIGABRT)):
@ -811,6 +855,11 @@ class TelegramBareClient:
for sig in stop_signals:
signal(sig, self._signal_handler)
if self._on_read_thread():
__log__.info('Starting to wait for items from the network')
else:
__log__.info('Idling to receive items from the network')
while self._user_connected:
try:
if datetime.now() > self._last_ping + self._ping_delay:
@ -819,14 +868,20 @@ class TelegramBareClient:
))
self._last_ping = datetime.now()
__log__.debug('Receiving items from the network...')
self._sender.receive(update_state=self.updates)
except TimeoutError:
# No problem.
pass
# No problem
__log__.info('Receiving items from the network timed out')
except ConnectionResetError:
self._logger.debug('Server disconnected us. Reconnecting...')
while self._user_connected and not self._reconnect():
sleep(0.1) # Retry forever, this is instant messaging
if self._user_connected:
__log__.error('Connection was reset while receiving '
'items. Reconnecting')
with self._reconnect_lock:
while self._user_connected and not self._reconnect():
sleep(0.1) # Retry forever, this is instant messaging
__log__.info('Connection closed by the user, not reading anymore')
# By using this approach, another thread will be
# created and started upon connection to constantly read
@ -843,11 +898,9 @@ class TelegramBareClient:
try:
self.idle(stop_signals=tuple())
except Exception as error:
__log__.exception('Unknown exception in the read thread! '
'Disconnecting and leaving it to main thread')
# Unknown exception, pass it to the main thread
self._logger.error(
'Unknown error on the read thread, please report',
error
)
try:
import socks
@ -861,12 +914,7 @@ class TelegramBareClient:
self.disconnect()
break
except ImportError:
"Not using PySocks, so it can't be a socket error"
# If something strange happens we don't want to enter an
# infinite loop where all we do is raise an exception, so
# add a little sleep to avoid the CPU usage going mad.
sleep(0.1)
"Not using PySocks, so it can't be a proxy error"
self._recv_thread = None

View File

@ -1,4 +1,7 @@
import itertools
import os
import time
from collections import OrderedDict
from datetime import datetime, timedelta
from mimetypes import guess_type
@ -15,22 +18,23 @@ from .errors import (
)
from .network import ConnectionMode
from .tl import TLObject
from .tl.custom import Draft
from .tl.custom import Draft, Dialog
from .tl.entity_database import EntityDatabase
from .tl.functions.account import (
GetPasswordRequest
)
from .tl.functions.auth import (
CheckPasswordRequest, LogOutRequest, SendCodeRequest, SignInRequest,
SignUpRequest, ImportBotAuthorizationRequest
SignUpRequest, ResendCodeRequest, ImportBotAuthorizationRequest
)
from .tl.functions.contacts import (
GetContactsRequest, ResolveUsernameRequest
)
from .tl.functions.messages import (
GetDialogsRequest, GetHistoryRequest, ReadHistoryRequest, SendMediaRequest,
SendMessageRequest, GetChatsRequest,
GetAllDraftsRequest)
SendMessageRequest, GetChatsRequest, GetAllDraftsRequest,
CheckChatInviteRequest
)
from .tl.functions import channels
from .tl.functions import messages
@ -48,8 +52,11 @@ from .tl.types import (
Message, MessageMediaContact, MessageMediaDocument, MessageMediaPhoto,
InputUserSelf, UserProfilePhoto, ChatPhoto, UpdateMessageID,
UpdateNewChannelMessage, UpdateNewMessage, UpdateShortSentMessage,
PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel)
PeerUser, InputPeerUser, InputPeerChat, InputPeerChannel, MessageEmpty,
ChatInvite, ChatInviteAlready, PeerChannel
)
from .tl.types.messages import DialogsSlice
from .extensions import markdown
class TelegramClient(TelegramBareClient):
@ -64,6 +71,7 @@ class TelegramClient(TelegramBareClient):
def __init__(self, session, api_id, api_hash,
connection_mode=ConnectionMode.TCP_FULL,
use_ipv6=False,
proxy=None,
update_workers=None,
timeout=timedelta(seconds=5),
@ -108,6 +116,7 @@ class TelegramClient(TelegramBareClient):
super().__init__(
session, api_id, api_hash,
connection_mode=connection_mode,
use_ipv6=use_ipv6,
proxy=proxy,
update_workers=update_workers,
spawn_read_thread=spawn_read_thread,
@ -125,16 +134,30 @@ class TelegramClient(TelegramBareClient):
# region Authorization requests
def send_code_request(self, phone):
def send_code_request(self, phone, force_sms=False):
"""Sends a code request to the specified phone number.
:param str | int phone: The phone to which the code will be sent.
:return auth.SentCode: Information about the result of the request.
:param str | int phone:
The phone to which the code will be sent.
:param bool force_sms:
Whether to force sending as SMS.
:return auth.SentCode:
Information about the result of the request.
"""
phone = EntityDatabase.parse_phone(phone) or self._phone
result = self(SendCodeRequest(phone, self.api_id, self.api_hash))
if not self._phone_code_hash:
result = self(SendCodeRequest(phone, self.api_id, self.api_hash))
self._phone_code_hash = result.phone_code_hash
else:
force_sms = True
self._phone = phone
self._phone_code_hash = result.phone_code_hash
if force_sms:
result = self(ResendCodeRequest(phone, self._phone_code_hash))
self._phone_code_hash = result.phone_code_hash
return result
def sign_in(self, phone=None, code=None,
@ -273,38 +296,34 @@ class TelegramClient(TelegramBareClient):
The message ID to be used as an offset.
:param offset_peer:
The peer to be used as an offset.
:return: A tuple of lists ([dialogs], [entities]).
"""
if limit is None:
limit = float('inf')
dialogs = {} # Use peer id as identifier to avoid dupes
messages = {} # Used later for sorting TODO also return these?
entities = {}
:return List[telethon.tl.custom.Dialog]: A list dialogs.
"""
limit = float('inf') if limit is None else int(limit)
if limit == 0:
return [], []
dialogs = OrderedDict() # Use peer id as identifier to avoid dupes
while len(dialogs) < limit:
need = limit - len(dialogs)
real_limit = min(limit - len(dialogs), 100)
r = self(GetDialogsRequest(
offset_date=offset_date,
offset_id=offset_id,
offset_peer=offset_peer,
limit=need if need < float('inf') else 0
limit=real_limit
))
if not r.dialogs:
break
messages = {m.id: m for m in r.messages}
entities = {utils.get_peer_id(x, add_mark=True): x
for x in itertools.chain(r.users, r.chats)}
for d in r.dialogs:
dialogs[utils.get_peer_id(d.peer, True)] = d
for m in r.messages:
messages[m.id] = m
dialogs[utils.get_peer_id(d.peer, add_mark=True)] = \
Dialog(self, d, entities, messages)
# We assume users can't have the same ID as a chat
for u in r.users:
entities[u.id] = u
for c in r.chats:
entities[c.id] = c
if not isinstance(r, DialogsSlice):
# Don't enter next iteration if we already got all
if len(r.dialogs) < real_limit or not isinstance(r, DialogsSlice):
# Less than we requested means we reached the end, or
# we didn't get a DialogsSlice which means we got all.
break
offset_date = r.messages[-1].date
@ -313,20 +332,8 @@ class TelegramClient(TelegramBareClient):
)
offset_id = r.messages[-1].id & 4294967296 # Telegram/danog magic
# Sort by message date. Windows will raise if timestamp is 0,
# so we need to set at least one day ahead while still being
# the smallest date possible.
no_date = datetime.fromtimestamp(86400)
ds = list(sorted(
dialogs.values(),
key=lambda d: getattr(messages[d.top_message], 'date', no_date)
))
if limit < float('inf'):
ds = ds[:limit]
return (
ds,
[utils.find_user_or_chat(d.peer, entities, entities) for d in ds]
)
dialogs = list(dialogs.values())
return dialogs[:limit] if limit < float('inf') else dialogs
def get_drafts(self): # TODO: Ability to provide a `filter`
"""
@ -348,21 +355,39 @@ class TelegramClient(TelegramBareClient):
entity,
message,
reply_to=None,
parse_mode=None,
link_preview=True):
"""
Sends the given message to the specified entity (user/chat/channel).
:param str | int | User | Chat | Channel entity: To who will it be sent.
:param str message: The message to be sent.
:param int | Message reply_to: Whether to reply to a message or not.
:param link_preview: Should the link preview be shown?
:param str | int | User | Chat | Channel entity:
To who will it be sent.
:param str message:
The message to be sent.
:param int | Message reply_to:
Whether to reply to a message or not.
:param str parse_mode:
Can be 'md' or 'markdown' for markdown-like parsing, in a similar
fashion how official clients work.
:param link_preview:
Should the link preview be shown?
:return Message: the sent message
"""
entity = self.get_input_entity(entity)
if parse_mode:
parse_mode = parse_mode.lower()
if parse_mode in {'md', 'markdown'}:
message, msg_entities = markdown.parse(message)
else:
raise ValueError('Unknown parsing mode', parse_mode)
else:
msg_entities = []
request = SendMessageRequest(
peer=entity,
message=message,
entities=[],
entities=msg_entities,
no_webpage=not link_preview,
reply_to_msg_id=self._get_reply_to(reply_to)
)
@ -438,43 +463,100 @@ class TelegramClient(TelegramBareClient):
"""
Gets the message history for the specified entity
:param entity: The entity from whom to retrieve the message history
:param limit: Number of messages to be retrieved
:param offset_date: Offset date (messages *previous* to this date will be retrieved)
:param offset_id: Offset message ID (only messages *previous* to the given ID will be retrieved)
:param max_id: All the messages with a higher (newer) ID or equal to this will be excluded
:param min_id: All the messages with a lower (older) ID or equal to this will be excluded
:param add_offset: Additional message offset (all of the specified offsets + this offset = older messages)
:param entity:
The entity from whom to retrieve the message history.
:param limit:
Number of messages to be retrieved. Due to limitations with the API
retrieving more than 3000 messages will take longer than half a
minute (or even more based on previous calls). The limit may also
be None, which would eventually return the whole history.
:param offset_date:
Offset date (messages *previous* to this date will be retrieved).
:param offset_id:
Offset message ID (only messages *previous* to the given ID will
be retrieved).
:param max_id:
All the messages with a higher (newer) ID or equal to this will
be excluded
:param min_id:
All the messages with a lower (older) ID or equal to this will
be excluded.
:param add_offset:
Additional message offset
(all of the specified offsets + this offset = older messages).
:return: A tuple containing total message count and two more lists ([messages], [senders]).
Note that the sender can be null if it was not found!
"""
result = self(GetHistoryRequest(
peer=self.get_input_entity(entity),
limit=limit,
offset_date=offset_date,
offset_id=offset_id,
max_id=max_id,
min_id=min_id,
add_offset=add_offset
))
entity = self.get_input_entity(entity)
limit = float('inf') if limit is None else int(limit)
if limit == 0:
# No messages, but we still need to know the total message count
result = self(GetHistoryRequest(
peer=entity, limit=1,
offset_date=None, offset_id=0, max_id=0, min_id=0, add_offset=0
))
return getattr(result, 'count', len(result.messages)), [], []
# The result may be a messages slice (not all messages were retrieved)
# or simply a messages TLObject. In the later case, no "count"
# attribute is specified, so the total messages count is simply
# the count of retrieved messages
total_messages = getattr(result, 'count', len(result.messages))
total_messages = 0
messages = []
entities = {}
while len(messages) < limit:
# Telegram has a hard limit of 100
real_limit = min(limit - len(messages), 100)
result = self(GetHistoryRequest(
peer=entity,
limit=real_limit,
offset_date=offset_date,
offset_id=offset_id,
max_id=max_id,
min_id=min_id,
add_offset=add_offset
))
messages.extend(
m for m in result.messages if not isinstance(m, MessageEmpty)
)
total_messages = getattr(result, 'count', len(result.messages))
# Iterate over all the messages and find the sender User
entities = [
utils.find_user_or_chat(m.from_id, result.users, result.chats)
if m.from_id is not None else
utils.find_user_or_chat(m.to_id, result.users, result.chats)
# TODO We can potentially use self.session.database, but since
# it might be disabled, use a local dictionary.
for u in result.users:
entities[utils.get_peer_id(u, add_mark=True)] = u
for c in result.chats:
entities[utils.get_peer_id(c, add_mark=True)] = c
for m in result.messages
]
if len(result.messages) < real_limit:
break
return total_messages, result.messages, entities
offset_id = result.messages[-1].id
offset_date = result.messages[-1].date
# Telegram limit seems to be 3000 messages within 30 seconds in
# batches of 100 messages each request (since the FloodWait was
# of 30 seconds). If the limit is greater than that, we will
# sleep 1s between each request.
if limit > 3000:
time.sleep(1)
# In a new list with the same length as the messages append
# their senders, so people can zip(messages, senders).
senders = []
for m in messages:
if m.from_id:
who = entities[utils.get_peer_id(m.from_id, add_mark=True)]
elif getattr(m, 'fwd_from', None):
# .from_id is optional, so this is the sanest fallback.
who = entities[utils.get_peer_id(
m.fwd_from.from_id or PeerChannel(m.fwd_from.channel_id),
add_mark=True
)]
else:
# If there's not even a FwdHeader, fallback to the sender
# being where the message was sent.
who = entities[utils.get_peer_id(m.to_id, add_mark=True)]
senders.append(who)
return total_messages, messages, senders
def send_read_acknowledge(self, entity, messages=None, max_id=None):
"""
@ -821,14 +903,14 @@ class TelegramClient(TelegramBareClient):
f = file
try:
# Remove these pesky characters
first_name = first_name.replace(';', '')
last_name = (last_name or '').replace(';', '')
f.write('BEGIN:VCARD\n')
f.write('VERSION:4.0\n')
f.write('N:{};{};;;\n'.format(
first_name, last_name if last_name else '')
)
f.write('FN:{}\n'.format(' '.join((first_name, last_name))))
f.write('TEL;TYPE=cell;VALUE=uri:tel:+{}\n'.format(
phone_number))
f.write('N:{};{};;;\n'.format(first_name, last_name))
f.write('FN:{} {}\n'.format(first_name, last_name))
f.write('TEL;TYPE=cell;VALUE=uri:tel:+{}\n'.format(phone_number))
f.write('END:VCARD\n')
finally:
# Only close the stream if we opened it
@ -901,7 +983,7 @@ class TelegramClient(TelegramBareClient):
# region Small utilities to make users' life easier
def get_entity(self, entity):
def get_entity(self, entity, force_fetch=False):
"""
Turns the given entity into a valid Telegram user or chat.
@ -919,12 +1001,20 @@ class TelegramClient(TelegramBareClient):
If the entity is neither, and it's not a TLObject, an
error will be raised.
:param force_fetch:
If True, the entity cache is bypassed and the entity is fetched
again with an API call. Defaults to False to avoid unnecessary
calls, but since a cached version would be returned, the entity
may be out of date.
:return:
"""
try:
return self.session.entities[entity]
except KeyError:
pass
if not force_fetch:
# Try to use cache unless we want to force a fetch
try:
return self.session.entities[entity]
except KeyError:
pass
if isinstance(entity, int) or (
isinstance(entity, TLObject) and
@ -961,8 +1051,18 @@ class TelegramClient(TelegramBareClient):
entity = phone
self(GetContactsRequest(0))
else:
entity = string.strip('@').lower()
self(ResolveUsernameRequest(entity))
entity, is_join_chat = EntityDatabase.parse_username(string)
if is_join_chat:
invite = self(CheckChatInviteRequest(entity))
if isinstance(invite, ChatInvite):
# If it's an invite to a chat, the user must join before
# for the link to be resolved and work, otherwise raise.
if invite.channel:
return invite.channel
elif isinstance(invite, ChatInviteAlready):
return invite.chat
else:
self(ResolveUsernameRequest(entity))
# MtProtoSender will call .process_entities on the requests made
try:
return self.session.entities[entity]
@ -1039,4 +1139,4 @@ class TelegramClient(TelegramBareClient):
'Make sure you have encountered this peer before.'.format(peer)
)
# endregion
# endregion

View File

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

View File

@ -0,0 +1,37 @@
from . import Draft
from ... import utils
class Dialog:
"""
Custom class that encapsulates a dialog (an open "conversation" with
someone, a group or a channel) providing an abstraction to easily
access the input version/normal entity/message etc. The library will
return instances of this class when calling `client.get_dialogs()`.
"""
def __init__(self, client, dialog, entities, messages):
# Both entities and messages being dicts {ID: item}
self._client = client
self.dialog = dialog
self.pinned = bool(dialog.pinned)
self.message = messages.get(dialog.top_message, None)
self.date = getattr(self.message, 'date', None)
self.entity = entities[utils.get_peer_id(dialog.peer, add_mark=True)]
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
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 self._client.send_message(self.input_entity, *args, **kwargs)

View File

@ -9,6 +9,11 @@ from ..tl.types import (
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"
@ -24,7 +29,12 @@ class EntityDatabase:
self._entities = {} # marked_id: user|chat|channel
if input_list:
self._input_entities = {k: v for k, v in 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
@ -69,10 +79,22 @@ class EntityDatabase:
try:
p = utils.get_input_peer(e, allow_self=False)
new_input[utils.get_peer_id(p, add_mark=True)] = \
getattr(p, 'access_hash', 0) # chats won't have hash
marked_id = utils.get_peer_id(p, add_mark=True)
if self.enabled_full:
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:
@ -118,7 +140,7 @@ class EntityDatabase:
phone = getattr(entity, 'phone', None)
if phone:
self._username_id[phone] = marked_id
self._phone_id[phone] = marked_id
def _parse_key(self, key):
"""Parses the given string, integer or TLObject key into a
@ -136,7 +158,8 @@ class EntityDatabase:
if phone:
return self._phone_id[phone]
else:
return self._username_id[key.lstrip('@').lower()]
username, _ = EntityDatabase.parse_username(key)
return self._username_id[username.lower()]
except KeyError as e:
raise ValueError() from e
@ -189,6 +212,19 @@ class EntityDatabase:
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)

View File

@ -11,15 +11,20 @@ class MessageContainer(TLObject):
self.content_related = False
self.messages = messages
def to_dict(self, recursive=True):
return {
'content_related': self.content_related,
'messages':
([] if self.messages is None else [
None if x is None else x.to_dict() for x in self.messages
]) if recursive else self.messages,
}
def __bytes__(self):
return struct.pack(
'<Ii', MessageContainer.CONSTRUCTOR_ID, len(self.messages)
) + b''.join(bytes(m) for m in self.messages)
def __str__(self):
return 'MessageContainer(messages=[{}])'\
.format(', '.join(str(m) for m in self.messages))
@staticmethod
def iter_read(reader):
reader.read_int(signed=False) # code
@ -29,3 +34,9 @@ class MessageContainer(TLObject):
inner_sequence = reader.read_int()
inner_length = reader.read_int()
yield inner_msg_id, inner_sequence, inner_length
def __str__(self):
return TLObject.pretty_format(self)
def stringify(self):
return TLObject.pretty_format(self, indent=0)

View File

@ -1,6 +1,7 @@
import json
import os
import platform
import struct
import time
from base64 import b64encode, b64decode
from os.path import isfile as file_exists
@ -58,17 +59,17 @@ class Session:
self._msg_id_lock = Lock()
self._save_lock = Lock()
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
# These values will be saved
self.server_address = '91.108.56.165'
self.port = 443
self.server_address = None
self.port = None
self.auth_key = None
self.layer = 0
self.salt = 0 # Unsigned long
self.salt = 0 # Signed long
self.entities = EntityDatabase() # Known and cached entities
def save(self):
@ -126,6 +127,11 @@ class Session:
data = json.load(file)
result.port = data.get('port', result.port)
result.salt = data.get('salt', result.salt)
# Keep while migrating from unsigned to signed salt
if result.salt > 0:
result.salt = struct.unpack(
'q', struct.pack('Q', result.salt))[0]
result.layer = data.get('layer', result.layer)
result.server_address = \
data.get('server_address', result.server_address)

View File

@ -13,10 +13,20 @@ class TLMessage(TLObject):
self.request = request
self.container_msg_id = None
def to_dict(self, recursive=True):
return {
'msg_id': self.msg_id,
'seq_no': self.seq_no,
'request': self.request,
'container_msg_id': self.container_msg_id,
}
def __bytes__(self):
body = GzipPacked.gzip_if_smaller(self.request)
return struct.pack('<qii', self.msg_id, self.seq_no, len(body)) + body
def __str__(self):
return 'TLMessage(msg_id={}, seq_no={}, body={})'\
.format(self.msg_id, self.seq_no, self.request)
return TLObject.pretty_format(self)
def stringify(self):
return TLObject.pretty_format(self, indent=0)

View File

@ -4,8 +4,6 @@ from threading import Event
class TLObject:
def __init__(self):
self.request_msg_id = 0 # Long
self.confirm_received = Event()
self.rpc_error = None
@ -36,7 +34,9 @@ class TLObject:
', '.join(TLObject.pretty_format(x) for x in obj)
)
elif isinstance(obj, datetime):
return 'datetime.fromtimestamp({})'.format(obj.timestamp())
return 'datetime.utcfromtimestamp({})'.format(
int(obj.timestamp())
)
else:
return repr(obj)
else:
@ -81,8 +81,8 @@ class TLObject:
result.append(']')
elif isinstance(obj, datetime):
result.append('datetime.fromtimestamp(')
result.append(repr(obj.timestamp()))
result.append('datetime.utcfromtimestamp(')
result.append(repr(int(obj.timestamp())))
result.append(')')
else:

View File

@ -1,11 +1,14 @@
import logging
import pickle
from collections import deque
from queue import Queue, Empty
from datetime import datetime
from threading import RLock, Event, Thread
from threading import RLock, Thread
from .tl import types as tl
__log__ = logging.getLogger(__name__)
class UpdateState:
"""Used to hold the current state of processed updates.
@ -26,35 +29,26 @@ class UpdateState:
self.handlers = []
self._updates_lock = RLock()
self._updates_available = Event()
self._updates = deque()
self._updates = Queue()
self._latest_updates = deque(maxlen=10)
self._logger = logging.getLogger(__name__)
# https://core.telegram.org/api/updates
self._state = tl.updates.State(0, 0, datetime.now(), 0, 0)
def can_poll(self):
"""Returns True if a call to .poll() won't lock"""
return self._updates_available.is_set()
return not self._updates.empty()
def poll(self, timeout=None):
"""Polls an update or blocks until an update object is available.
If 'timeout is not None', it should be a floating point value,
and the method will 'return None' if waiting times out.
"""
if not self._updates_available.wait(timeout=timeout):
try:
update = self._updates.get(timeout=timeout)
except Empty:
return
with self._updates_lock:
if not self._updates_available.is_set():
return
update = self._updates.popleft()
if not self._updates:
self._updates_available.clear()
if isinstance(update, Exception):
raise update # Some error was set through (surely StopIteration)
@ -70,7 +64,8 @@ class UpdateState:
self.stop_workers()
self._workers = n
if n is None:
self._updates.clear()
while self._updates:
self._updates.get()
else:
self.setup_workers()
@ -86,8 +81,7 @@ class UpdateState:
# on all the worker threads
# TODO Should this reset the pts and such?
for _ in range(self._workers):
self._updates.appendleft(StopIteration())
self._updates_available.set()
self._updates.put(StopIteration())
for t in self._worker_threads:
t.join()
@ -121,9 +115,7 @@ class UpdateState:
break
except:
# We don't want to crash a worker thread due to any reason
self._logger.exception(
'[ERROR] Unhandled exception on worker {}'.format(wid)
)
__log__.exception('Unhandled exception on worker %d', wid)
def process(self, update):
"""Processes an update object. This method is normally called by
@ -134,11 +126,13 @@ class UpdateState:
with self._updates_lock:
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
@ -159,31 +153,19 @@ 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._updates.append(update.update)
self._updates_available.set()
elif isinstance(update, (tl.Updates, tl.UpdatesCombined)):
self._updates.extend(update.updates)
self._updates_available.set()
elif not isinstance(update, tl.UpdatesTooLong):
# TODO Handle "Updates too long"
self._updates.append(update)
self._updates_available.set()
elif type(update).SUBCLASS_OF_ID == 0x9f89304e: # crc32(b'Update')
self._updates.append(update)
self._updates_available.set()
if isinstance(update, tl.UpdateShort):
self._updates.put(update.update)
# Expand "Updates" into "Update", and pass these to callbacks.
# Since .users and .chats have already been processed, we
# don't need to care about those either.
elif isinstance(update, (tl.Updates, tl.UpdatesCombined)):
for u in update.updates:
self._updates.put(u)
# TODO Handle "tl.UpdatesTooLong"
else:
self._logger.debug('Ignoring "update" of type {}'.format(
type(update).__name__)
)
self._updates.put(update)

View File

@ -19,7 +19,7 @@ from .tl.types import (
DocumentEmpty, InputDocumentEmpty, Message, GeoPoint, InputGeoPoint,
GeoPointEmpty, InputGeoPointEmpty, Photo, InputPhoto, PhotoEmpty,
InputPhotoEmpty, FileLocation, ChatPhotoEmpty, UserProfilePhotoEmpty,
FileLocationUnavailable, InputMediaUploadedDocument,
FileLocationUnavailable, InputMediaUploadedDocument, ChannelFull,
InputMediaUploadedPhoto, DocumentAttributeFilename, photos
)
@ -35,12 +35,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')
@ -84,13 +84,13 @@ def get_input_peer(entity, allow_self=True):
if entity.is_self and allow_self:
return InputPeerSelf()
else:
return InputPeerUser(entity.id, entity.access_hash)
return InputPeerUser(entity.id, entity.access_hash or 0)
if isinstance(entity, (Chat, ChatEmpty, ChatForbidden)):
return InputPeerChat(entity.id)
if isinstance(entity, (Channel, ChannelForbidden)):
return InputPeerChannel(entity.id, entity.access_hash)
return InputPeerChannel(entity.id, entity.access_hash or 0)
# Less common cases
if isinstance(entity, UserEmpty):
@ -99,6 +99,9 @@ def get_input_peer(entity, allow_self=True):
if isinstance(entity, InputUser):
return InputPeerUser(entity.user_id, entity.access_hash)
if isinstance(entity, InputUserSelf):
return InputPeerSelf()
if isinstance(entity, UserFull):
return get_input_peer(entity.user)
@ -120,7 +123,7 @@ def get_input_channel(entity):
return entity
if isinstance(entity, (Channel, ChannelForbidden)):
return InputChannel(entity.id, entity.access_hash)
return InputChannel(entity.id, entity.access_hash or 0)
if isinstance(entity, InputPeerChannel):
return InputChannel(entity.channel_id, entity.access_hash)
@ -140,7 +143,7 @@ def get_input_user(entity):
if entity.is_self:
return InputUserSelf()
else:
return InputUser(entity.id, entity.access_hash)
return InputUser(entity.id, entity.access_hash or 0)
if isinstance(entity, InputPeerSelf):
return InputUserSelf()
@ -322,8 +325,13 @@ def get_peer_id(peer, add_mark=False):
return peer.user_id
elif isinstance(peer, (PeerChat, InputPeerChat)):
return -peer.chat_id if add_mark else peer.chat_id
elif isinstance(peer, (PeerChannel, InputPeerChannel)):
i = peer.channel_id
elif isinstance(peer, (PeerChannel, InputPeerChannel, ChannelFull)):
if isinstance(peer, ChannelFull):
# Special case: .get_input_peer can't return InputChannel from
# ChannelFull since it doesn't have an .access_hash attribute.
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

3
telethon/version.py Normal file
View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -354,7 +354,7 @@ class InteractiveTelegramClient(TelegramClient):
update.message, get_display_name(who)
))
else:
sprint('<< {} sent "{}"]'.format(
sprint('<< {} sent "{}"'.format(
get_display_name(who), update.message
))

View File

@ -1,43 +1,116 @@
#!/usr/bin/env python3
# A script to automatically send messages based on certain triggers
"""
A example script to automatically send messages based on certain triggers.
The script makes uses of environment variables to determine the API ID,
hash, phone and such to be used. You may want to add these to your .bashrc
file, including TG_API_ID, TG_API_HASH, TG_PHONE and optionally TG_SESSION.
This script assumes that you have certain files on the working directory,
such as "xfiles.m4a" or "anytime.png" for some of the automated replies.
"""
from getpass import getpass
from collections import defaultdict
from datetime import datetime, timedelta
from os import environ
# environ is used to get API information from environment variables
# You could also use a config file, pass them as arguments,
# or even hardcode them (not recommended)
from nltk.tokenize import word_tokenize
# NLTK is used to match specific triggers in messages
import re
from telethon import TelegramClient
from telethon.errors import SessionPasswordNeededError
from telethon.tl.types import UpdateNewChannelMessage, UpdateShortMessage, MessageService
from telethon.tl.functions.messages import EditMessageRequest
# Uncomment this for debugging
# import logging
# logging.basicConfig(level=logging.DEBUG)
# logging.debug('dbg')
# logging.info('info')
"""Uncomment this for debugging
import logging
logging.basicConfig(level=logging.DEBUG)
logging.debug('dbg')
logging.info('info')
"""
REACTS = {'emacs': 'Needs more vim',
'chrome': 'Needs more Firefox'}
def setup():
# A list of dates of reactions we've sent, so we can keep track of floods
recent_reacts = defaultdict(list)
def update_handler(update):
global recent_reacts
try:
global recent_reacts
# A list of dates of reactions we've sent, so we can keep track of floods
recent_reacts = defaultdict(list)
msg = update.message
except AttributeError:
# print(update, 'did not have update.message')
return
if isinstance(msg, MessageService):
print(msg, 'was service msg')
return
global client
session_name = environ.get('TG_SESSION', 'session')
user_phone = environ['TG_PHONE']
client = TelegramClient(session_name,
int(environ['TG_API_ID']),
environ['TG_API_HASH'],
proxy=None,
update_workers=4)
# React to messages in supergroups and PMs
if isinstance(update, UpdateNewChannelMessage):
words = re.split('\W+', msg.message)
for trigger, response in REACTS.items():
if len(recent_reacts[msg.to_id.channel_id]) > 3:
# Silently ignore triggers if we've recently sent 3 reactions
break
if trigger in words:
# Remove recent replies older than 10 minutes
recent_reacts[msg.to_id.channel_id] = [
a for a in recent_reacts[msg.to_id.channel_id] if
datetime.now() - a < timedelta(minutes=10)
]
# Send a reaction
client.send_message(msg.to_id, response, reply_to=msg.id)
# Add this reaction to the list of recent actions
recent_reacts[msg.to_id.channel_id].append(datetime.now())
if isinstance(update, UpdateShortMessage):
words = re.split('\W+', msg)
for trigger, response in REACTS.items():
if len(recent_reacts[update.user_id]) > 3:
# Silently ignore triggers if we've recently sent 3 reactions
break
if trigger in words:
# Send a reaction
client.send_message(update.user_id, response, reply_to=update.id)
# Add this reaction to the list of recent reactions
recent_reacts[update.user_id].append(datetime.now())
# Automatically send relevant media when we say certain things
# When invoking requests, get_input_entity needs to be called manually
if isinstance(update, UpdateNewChannelMessage) and msg.out:
if msg.message.lower() == 'x files theme':
client.send_voice_note(msg.to_id, 'xfiles.m4a', reply_to=msg.id)
if msg.message.lower() == 'anytime':
client.send_file(msg.to_id, 'anytime.png', reply_to=msg.id)
if '.shrug' in msg.message:
client(EditMessageRequest(
client.get_input_entity(msg.to_id), msg.id,
message=msg.message.replace('.shrug', r'¯\_(ツ)_/¯')
))
if isinstance(update, UpdateShortMessage) and update.out:
if msg.lower() == 'x files theme':
client.send_voice_note(update.user_id, 'xfiles.m4a', reply_to=update.id)
if msg.lower() == 'anytime':
client.send_file(update.user_id, 'anytime.png', reply_to=update.id)
if '.shrug' in msg:
client(EditMessageRequest(
client.get_input_entity(update.user_id), update.id,
message=msg.replace('.shrug', r'¯\_(ツ)_/¯')
))
if __name__ == '__main__':
session_name = environ.get('TG_SESSION', 'session')
user_phone = environ['TG_PHONE']
client = TelegramClient(
session_name, int(environ['TG_API_ID']), environ['TG_API_HASH'],
proxy=None, update_workers=4
)
try:
print('INFO: Connecting to Telegram Servers...', end='', flush=True)
client.connect()
print('Done!')
@ -54,82 +127,11 @@ def setup():
password = getpass('Two step verification enabled. '
'Please enter your password: ')
code_ok = client.sign_in(password=password)
print('INFO: Client initialized succesfully!')
print('INFO: Client initialized successfully!')
client.add_update_handler(update_handler)
input('Press Enter to stop this!\n')
except KeyboardInterrupt:
pass
finally:
client.disconnect()
def update_handler(update):
global recent_reacts
try:
msg = update.message
except AttributeError:
# print(update, 'did not have update.message')
return
if isinstance(msg, MessageService):
print(msg, 'was service msg')
return
# React to messages in supergroups and PMs
if isinstance(update, UpdateNewChannelMessage):
words = word_tokenize(msg.message)
for trigger, response in REACTS.items():
if len(recent_reacts[msg.to_id.channel_id]) > 3:
break
# Silently ignore triggers if we've recently sent three reactions
if trigger in words:
recent_reacts[msg.to_id.channel_id] = [
a for a in recent_reacts[msg.to_id.channel_id] if
datetime.now() - a < timedelta(minutes=10)]
# Remove recents older than 10 minutes
client.send_message(msg.to_id, response, reply_to=msg.id)
# Send a reaction
recent_reacts[msg.to_id.channel_id].append(datetime.now())
# Add this reaction to the recents list
if isinstance(update, UpdateShortMessage):
words = word_tokenize(msg)
for trigger, response in REACTS.items():
if len(recent_reacts[update.user_id]) > 3:
break
# Silently ignore triggers if we've recently sent three reactions
if trigger in words:
recent_reacts[update.user_id] = [
a for a in recent_reacts[update.user_id] if
datetime.now() - a < timedelta(minutes=10)]
# Remove recents older than 10 minutes
client.send_message(update.user_id, response, reply_to=update.id)
# Send a reaction
recent_reacts[update.user_id].append(datetime.now())
# Add this reaction to the recents list
# Automatically send relevant media when we say certain things
# When invoking requests, get_input_entity needs to be called manually
if isinstance(update, UpdateNewChannelMessage) and msg.out:
if msg.message.lower() == 'x files theme':
client.send_voice_note(msg.to_id, 'xfiles.m4a', reply_to=msg.id)
if msg.message.lower() == 'anytime':
client.send_file(msg.to_id, 'anytime.png', reply_to=msg.id)
if '.shrug' in msg.message:
client(
EditMessageRequest(client.get_input_entity(msg.to_id), msg.id,
message=msg.message.replace('.shrug', r'¯\_(ツ)_/¯')))
if isinstance(update, UpdateShortMessage) and update.out:
if msg.lower() == 'x files theme':
client.send_voice_note(update.user_id, 'xfiles.m4a', reply_to=update.id)
if msg.lower() == 'anytime':
client.send_file(update.user_id, 'anytime.png', reply_to=update.id)
if '.shrug' in msg:
client(
EditMessageRequest(client.get_input_entity(update.user_id), update.id,
message=msg.replace('.shrug', r'¯\_(ツ)_/¯')))
if __name__ == '__main__':
setup()

View File

@ -45,7 +45,7 @@ PHONE_NUMBER_OCCUPIED=The phone number is already in use
PHONE_NUMBER_UNOCCUPIED=The phone number is not yet being used
PHOTO_INVALID_DIMENSIONS=The photo dimensions are invalid
TYPE_CONSTRUCTOR_INVALID=The type constructor is invalid
USERNAME_INVALID=Unacceptable username. Must match r"[a-zA-Z][\w\d]{3,30}[a-zA-Z\d]"
USERNAME_INVALID=Nobody is using this username, or the username is unacceptable. If the latter, it must match r"[a-zA-Z][\w\d]{3,30}[a-zA-Z\d]"
USERNAME_NOT_MODIFIED=The username is not different from the current username
USERNAME_NOT_OCCUPIED=The username is not in use by anyone else yet
USERNAME_OCCUPIED=The username is already taken

View File

@ -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)

View File

@ -159,14 +159,17 @@ inputMediaUploadedPhoto#2f37e231 flags:# file:InputFile caption:string stickers:
inputMediaPhoto#81fa373a flags:# id:InputPhoto caption:string ttl_seconds:flags.0?int = InputMedia;
inputMediaGeoPoint#f9c44144 geo_point:InputGeoPoint = InputMedia;
inputMediaContact#a6e45987 phone_number:string first_name:string last_name:string = InputMedia;
inputMediaUploadedDocument#e39621fd flags:# file:InputFile thumb:flags.2?InputFile mime_type:string attributes:Vector<DocumentAttribute> caption:string stickers:flags.0?Vector<InputDocument> ttl_seconds:flags.1?int = InputMedia;
inputMediaUploadedDocument#e39621fd flags:# nosound_video:flags.3?true file:InputFile thumb:flags.2?InputFile mime_type:string attributes:Vector<DocumentAttribute> caption:string stickers:flags.0?Vector<InputDocument> ttl_seconds:flags.1?int = InputMedia;
inputMediaDocument#5acb668e flags:# id:InputDocument caption:string ttl_seconds:flags.0?int = InputMedia;
inputMediaVenue#2827a81a geo_point:InputGeoPoint title:string address:string provider:string venue_id:string = InputMedia;
inputMediaVenue#c13d1c11 geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string = InputMedia;
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;
inputMediaGeoLive#7b1a118f geo_point:InputGeoPoint period:int = InputMedia;
inputSingleMedia#5eaa7809 media:InputMedia random_id:long = InputSingleMedia;
inputChatPhotoEmpty#1ca48f57 = InputChatPhoto;
inputChatUploadedPhoto#927c55b4 file:InputFile = InputChatPhoto;
@ -218,11 +221,11 @@ userStatusLastMonth#77ebc742 = UserStatus;
chatEmpty#9ba2d800 id:int = Chat;
chat#d91cdd54 flags:# creator:flags.0?true kicked:flags.1?true left:flags.2?true admins_enabled:flags.3?true admin:flags.4?true deactivated:flags.5?true id:int title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel = Chat;
chatForbidden#7328bdb id:int title:string = Chat;
channel#cb44b1c flags:# creator:flags.0?true left:flags.2?true editor:flags.3?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true democracy:flags.10?true signatures:flags.11?true min:flags.12?true id:int access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int version:int restriction_reason:flags.9?string admin_rights:flags.14?ChannelAdminRights banned_rights:flags.15?ChannelBannedRights = Chat;
channel#450b7115 flags:# creator:flags.0?true left:flags.2?true editor:flags.3?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true democracy:flags.10?true signatures:flags.11?true min:flags.12?true id:int access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int version:int restriction_reason:flags.9?string admin_rights:flags.14?ChannelAdminRights banned_rights:flags.15?ChannelBannedRights participants_count:flags.17?int = Chat;
channelForbidden#289da732 flags:# broadcast:flags.5?true megagroup:flags.8?true id:int access_hash:long title:string until_date:flags.16?int = Chat;
chatFull#2e02a614 id:int participants:ChatParticipants chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector<BotInfo> = ChatFull;
channelFull#17f45fcf flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet = ChatFull;
channelFull#76af5481 flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:Vector<BotInfo> migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int = ChatFull;
chatParticipant#c8d7493e user_id:int inviter_id:int date:int = ChatParticipant;
chatParticipantCreator#da13538a user_id:int = ChatParticipant;
@ -235,7 +238,7 @@ chatPhotoEmpty#37c1011c = ChatPhoto;
chatPhoto#6153276a photo_small:FileLocation photo_big:FileLocation = ChatPhoto;
messageEmpty#83e5de54 id:int = Message;
message#90dddc11 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true id:int from_id:flags.8?int to_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int edit_date:flags.15?int post_author:flags.16?string = Message;
message#44f9b43d flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true id:int from_id:flags.8?int to_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector<MessageEntity> views:flags.10?int edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long = Message;
messageService#9e19a1f6 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true id:int from_id:flags.8?int to_id:Peer reply_to_msg_id:flags.3?int date:int action:MessageAction = Message;
messageMediaEmpty#3ded6320 = MessageMedia;
@ -245,9 +248,10 @@ messageMediaContact#5e7d2f39 phone_number:string first_name:string last_name:str
messageMediaUnsupported#9f84f49e = MessageMedia;
messageMediaDocument#7c4414d3 flags:# document:flags.0?Document caption:flags.1?string ttl_seconds:flags.2?int = MessageMedia;
messageMediaWebPage#a32dd600 webpage:WebPage = MessageMedia;
messageMediaVenue#7912b71f geo:GeoPoint title:string address:string provider:string venue_id:string = MessageMedia;
messageMediaVenue#2ec0533f geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string = MessageMedia;
messageMediaGame#fdb19008 game:Game = MessageMedia;
messageMediaInvoice#84551347 flags:# shipping_address_requested:flags.1?true test:flags.3?true title:string description:string photo:flags.0?WebDocument receipt_msg_id:flags.2?int currency:string total_amount:long start_param:string = MessageMedia;
messageMediaGeoLive#7c3c2609 geo:GeoPoint period:int = MessageMedia;
messageActionEmpty#b6aef7b0 = MessageAction;
messageActionChatCreate#a6638b9a title:string users:Vector<int> = MessageAction;
@ -267,6 +271,7 @@ messageActionPaymentSentMe#8f31b327 flags:# currency:string total_amount:long pa
messageActionPaymentSent#40699cd0 currency:string total_amount:long = MessageAction;
messageActionPhoneCall#80e11a7f flags:# call_id:long reason:flags.0?PhoneCallDiscardReason duration:flags.1?int = MessageAction;
messageActionScreenshotTaken#4792929b = MessageAction;
messageActionCustomAction#fae69f56 message:string = MessageAction;
dialog#e4def5db flags:# pinned:flags.2?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage = Dialog;
@ -363,6 +368,8 @@ inputMessagesFilterPhoneCalls#80c99768 flags:# missed:flags.0?true = MessagesFil
inputMessagesFilterRoundVoice#7a7c17a4 = MessagesFilter;
inputMessagesFilterRoundVideo#b549da53 = MessagesFilter;
inputMessagesFilterMyMentions#c1f8e69a = MessagesFilter;
inputMessagesFilterContacts#e062db83 = MessagesFilter;
inputMessagesFilterGeo#e7026d0d = MessagesFilter;
updateNewMessage#1f2b0afd message:Message pts:int pts_count:int = Update;
updateMessageID#4e90bfd6 id:int random_id:long = Update;
@ -429,6 +436,7 @@ updateLangPack#56022f4d difference:LangPackDifference = Update;
updateFavedStickers#e511996d = Update;
updateChannelReadMessagesContents#89893b45 channel_id:int messages:Vector<int> = Update;
updateContactsReset#7084a7be = Update;
updateChannelAvailableMessages#70db6837 channel_id:int available_min_id:int = Update;
updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State;
@ -455,7 +463,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#8df376a4 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 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 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;
@ -665,6 +673,7 @@ channelParticipantsBanned#1427a5e1 q:string = ChannelParticipantsFilter;
channelParticipantsSearch#656ac4b q:string = ChannelParticipantsFilter;
channels.channelParticipants#f56ee2a8 count:int participants:Vector<ChannelParticipant> users:Vector<User> = channels.ChannelParticipants;
channels.channelParticipantsNotModified#f0173fe9 = channels.ChannelParticipants;
channels.channelParticipant#d0d9b163 participant:ChannelParticipant users:Vector<User> = channels.ChannelParticipant;
@ -680,7 +689,7 @@ messages.savedGifs#2e0709a5 hash:int gifs:Vector<Document> = messages.SavedGifs;
inputBotInlineMessageMediaAuto#292fed13 flags:# caption:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;
inputBotInlineMessageText#3dcd7a87 flags:# no_webpage:flags.0?true message:string entities:flags.1?Vector<MessageEntity> reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;
inputBotInlineMessageMediaGeo#f4a59de1 flags:# geo_point:InputGeoPoint reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;
inputBotInlineMessageMediaGeo#c1b15d65 flags:# geo_point:InputGeoPoint period:int reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;
inputBotInlineMessageMediaVenue#aaafadc8 flags:# geo_point:InputGeoPoint title:string address:string provider:string venue_id:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;
inputBotInlineMessageMediaContact#2daf01a7 flags:# phone_number:string first_name:string last_name:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;
inputBotInlineMessageGame#4b425864 flags:# reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;
@ -692,18 +701,18 @@ inputBotInlineResultGame#4fa417f2 id:string short_name:string send_message:Input
botInlineMessageMediaAuto#a74b15b flags:# caption:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage;
botInlineMessageText#8c7f65e2 flags:# no_webpage:flags.0?true message:string entities:flags.1?Vector<MessageEntity> reply_markup:flags.2?ReplyMarkup = BotInlineMessage;
botInlineMessageMediaGeo#3a8fd8b8 flags:# geo:GeoPoint reply_markup:flags.2?ReplyMarkup = BotInlineMessage;
botInlineMessageMediaGeo#b722de65 flags:# geo:GeoPoint period:int reply_markup:flags.2?ReplyMarkup = BotInlineMessage;
botInlineMessageMediaVenue#4366232e flags:# geo:GeoPoint title:string address:string provider:string venue_id:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage;
botInlineMessageMediaContact#35edb4d4 flags:# phone_number:string first_name:string last_name:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage;
botInlineResult#9bebaeb9 flags:# id:string type:string title:flags.1?string description:flags.2?string url:flags.3?string thumb_url:flags.4?string content_url:flags.5?string content_type:flags.5?string w:flags.6?int h:flags.6?int duration:flags.7?int send_message:BotInlineMessage = BotInlineResult;
botInlineMediaResult#17db940b flags:# id:string type:string photo:flags.0?Photo document:flags.1?Document title:flags.2?string description:flags.3?string send_message:BotInlineMessage = BotInlineResult;
messages.botResults#ccd3563d flags:# gallery:flags.0?true query_id:long next_offset:flags.1?string switch_pm:flags.2?InlineBotSwitchPM results:Vector<BotInlineResult> cache_time:int = messages.BotResults;
messages.botResults#947ca848 flags:# gallery:flags.0?true query_id:long next_offset:flags.1?string switch_pm:flags.2?InlineBotSwitchPM results:Vector<BotInlineResult> cache_time:int users:Vector<User> = messages.BotResults;
exportedMessageLink#1f486803 link:string = ExportedMessageLink;
messageFwdHeader#fadff4ac flags:# from_id:flags.0?int date:int channel_id:flags.1?int channel_post:flags.2?int post_author:flags.3?string = MessageFwdHeader;
messageFwdHeader#559ebe6d flags:# from_id:flags.0?int date:int channel_id:flags.1?int channel_post:flags.2?int post_author:flags.3?string saved_from_peer:flags.4?Peer saved_from_msg_id:flags.4?int = MessageFwdHeader;
auth.codeTypeSms#72a3158c = auth.CodeType;
auth.codeTypeCall#741cd3e3 = auth.CodeType;
@ -903,6 +912,7 @@ channelAdminLogEventActionParticipantInvite#e31c34d8 participant:ChannelParticip
channelAdminLogEventActionParticipantToggleBan#e6d83d7e prev_participant:ChannelParticipant new_participant:ChannelParticipant = ChannelAdminLogEventAction;
channelAdminLogEventActionParticipantToggleAdmin#d5676710 prev_participant:ChannelParticipant new_participant:ChannelParticipant = ChannelAdminLogEventAction;
channelAdminLogEventActionChangeStickerSet#b1c3caa7 prev_stickerset:InputStickerSet new_stickerset:InputStickerSet = ChannelAdminLogEventAction;
channelAdminLogEventActionTogglePreHistoryHidden#5f5c95f1 new_value:Bool = ChannelAdminLogEventAction;
channelAdminLogEvent#3b5a3e40 id:long date:int user_id:int action:ChannelAdminLogEventAction = ChannelAdminLogEvent;
@ -917,6 +927,14 @@ 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;
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;
---functions---
invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X;
@ -1001,7 +1019,7 @@ messages.receivedMessages#5a954c0 max_id:int = Vector<ReceivedNotifyMessage>;
messages.setTyping#a3825e50 peer:InputPeer action:SendMessageAction = Bool;
messages.sendMessage#fa88427a flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> = Updates;
messages.sendMedia#c8f16791 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int media:InputMedia random_id:long reply_markup:flags.2?ReplyMarkup = Updates;
messages.forwardMessages#708e0195 flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true from_peer:InputPeer id:Vector<int> random_id:Vector<long> to_peer:InputPeer = Updates;
messages.forwardMessages#708e0195 flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true grouped:flags.9?true from_peer:InputPeer id:Vector<int> random_id:Vector<long> to_peer:InputPeer = Updates;
messages.reportSpam#cf1592db peer:InputPeer = Bool;
messages.hideReportSpam#a8f1709b peer:InputPeer = Bool;
messages.getPeerSettings#3672e09c peer:InputPeer = PeerSettings;
@ -1048,7 +1066,7 @@ messages.getInlineBotResults#514e999d flags:# bot:InputUser peer:InputPeer geo_p
messages.setInlineBotResults#eb5ea206 flags:# gallery:flags.0?true private:flags.1?true query_id:long results:Vector<InputBotInlineResult> cache_time:int next_offset:flags.2?string switch_pm:flags.3?InlineBotSwitchPM = Bool;
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#ce91e4ca flags:# no_webpage:flags.1?true peer:InputPeer id:int message:flags.11?string reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> = Updates;
messages.editMessage#5d1b8dd flags:# no_webpage:flags.1?true stop_geo_live:flags.12?true peer:InputPeer id:int message:flags.11?string reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> geo_point:flags.13?InputGeoPoint = Updates;
messages.editInlineBotMessage#130c2c85 flags:# no_webpage:flags.1?true id:InputBotInlineMessageID message:flags.11?string reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector<MessageEntity> = Bool;
messages.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;
@ -1080,6 +1098,9 @@ 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.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;
updates.getState#edd4882a = updates.State;
updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference;
@ -1108,13 +1129,14 @@ help.getAppChangelog#9010ef6f prev_app_version:string = Updates;
help.getTermsOfService#350170f3 = help.TermsOfService;
help.setBotUpdatesStatus#ec22cfcd pending_updates_count:int message:string = Bool;
help.getCdnConfig#52029342 = CdnConfig;
help.getRecentMeUrls#3dc0f114 referer:string = help.RecentMeUrls;
channels.readHistory#cc104937 channel:InputChannel max_id:int = Bool;
channels.deleteMessages#84c1fd4e channel:InputChannel id:Vector<int> = messages.AffectedMessages;
channels.deleteUserHistory#d10dd71b channel:InputChannel user_id:InputUser = messages.AffectedHistory;
channels.reportSpam#fe087810 channel:InputChannel user_id:InputUser id:Vector<int> = Bool;
channels.getMessages#93d7b347 channel:InputChannel id:Vector<int> = messages.Messages;
channels.getParticipants#24d98f92 channel:InputChannel filter:ChannelParticipantsFilter offset:int limit:int = channels.ChannelParticipants;
channels.getParticipants#123e05e9 channel:InputChannel filter:ChannelParticipantsFilter offset:int limit:int hash:int = channels.ChannelParticipants;
channels.getParticipant#546dd7a6 channel:InputChannel user_id:InputUser = channels.ChannelParticipant;
channels.getChannels#a7f6bbb id:Vector<InputChannel> = messages.Chats;
channels.getFullChannel#8736a09 channel:InputChannel = messages.ChatFull;
@ -1139,6 +1161,8 @@ channels.editBanned#bfd915cd channel:InputChannel user_id:InputUser banned_right
channels.getAdminLog#33ddf480 flags:# channel:InputChannel q:string events_filter:flags.0?ChannelAdminLogEventsFilter admins:flags.1?Vector<InputUser> max_id:long min_id:long limit:int = channels.AdminLogResults;
channels.setStickers#ea8ca4f9 channel:InputChannel stickerset:InputStickerSet = Bool;
channels.readMessageContents#eab5dc38 channel:InputChannel id:Vector<int> = Bool;
channels.deleteHistory#af369d42 channel:InputChannel max_id:int = Bool;
channels.togglePreHistoryHidden#eabbb94c channel:InputChannel enabled:Bool = Updates;
bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON;
bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool;
@ -1169,4 +1193,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 71
// LAYER 73

View File

@ -464,9 +464,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:
@ -491,11 +493,18 @@ class TLGenerator:
elif arg.flag_indicator:
# Calculate the flags with those items which are not None
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)
))
if not any(f.is_flag for f in args):
# 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', ")
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