mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-07-21 21:39:46 +03:00
Merge branch 'master' of github.com:lonamiwebs/Telethon
This commit is contained in:
commit
e4b471105a
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -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
20
readthedocs/Makefile
Normal 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
174
readthedocs/conf.py
Normal 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'),
|
||||
]
|
||||
|
||||
|
||||
|
59
readthedocs/extra/advanced-usage/bots.rst
Normal file
59
readthedocs/extra/advanced-usage/bots.rst
Normal 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
|
||||
))
|
||||
|
||||
It’s a bit verbose, but it has all the information you would need to
|
||||
show it visually (button rows, and buttons within each row, each with
|
||||
its own data).
|
||||
|
||||
__ 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
|
58
readthedocs/extra/advanced-usage/signing-in.rst
Normal file
58
readthedocs/extra/advanced-usage/signing-in.rst
Normal 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!
|
||||
|
324
readthedocs/extra/advanced-usage/users-and-chats.rst
Normal file
324
readthedocs/extra/advanced-usage/users-and-chats.rst
Normal 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 you’ve called ``.get_dialogs()`` for instance, and your
|
||||
friend was there, the library will know about them. For more, read about
|
||||
the :ref:`sessions`.
|
||||
|
||||
If you want to get a channel or chat by ID, you need to specify that
|
||||
they are a channel or a chat. The library can’t infer what they are by
|
||||
just their ID (unless the ID is marked, but this is only done
|
||||
internally), so you need to wrap the ID around a `Peer`__ object:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.types import PeerUser, PeerChat, PeerChannel
|
||||
my_user = client.get_entity(PeerUser(some_id))
|
||||
my_chat = client.get_entity(PeerChat(some_id))
|
||||
my_channel = client.get_entity(PeerChannel(some_id))
|
||||
|
||||
**Note** that most requests don’t ask for an ``User``, or a ``Chat``,
|
||||
but rather for ``InputUser``, ``InputChat``, and so on. If this is the
|
||||
case, you should prefer ``.get_input_entity()`` over ``.get_entity()``,
|
||||
as it will be immediate if you provide an ID (whereas ``.get_entity()``
|
||||
may need to find who the entity is first).
|
||||
|
||||
Via your open “chats” (dialogs)
|
||||
-------------------------------
|
||||
|
||||
.. note::
|
||||
Please read here: :ref:`retrieving-all-dialogs`.
|
||||
|
||||
Via ResolveUsernameRequest
|
||||
--------------------------
|
||||
|
||||
This is the request used by ``.get_entity`` internally, but you can also
|
||||
use it by hand:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.functions.contacts import ResolveUsernameRequest
|
||||
|
||||
result = client(ResolveUsernameRequest('username'))
|
||||
found_chats = result.chats
|
||||
found_users = result.users
|
||||
# result.peer may be a PeerUser, PeerChat or PeerChannel
|
||||
|
||||
See `Peer`__ for more information about this result.
|
||||
|
||||
Via MessageFwdHeader
|
||||
--------------------
|
||||
|
||||
If all you have is a `MessageFwdHeader`__ after you retrieved a bunch
|
||||
of messages, this gives you access to the ``from_id`` (if forwarded from
|
||||
an user) and ``channel_id`` (if forwarded from a channel). Invoking
|
||||
`GetMessagesRequest`__ also returns a list of ``chats`` and
|
||||
``users``, and you can find the desired entity there:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Logic to retrieve messages with `GetMessagesRequest´
|
||||
messages = foo()
|
||||
fwd_header = bar()
|
||||
|
||||
user = next(u for u in messages.users if u.id == fwd_header.from_id)
|
||||
channel = next(c for c in messages.chats if c.id == fwd_header.channel_id)
|
||||
|
||||
Or you can just call ``.get_entity()`` with the ID, as you should have
|
||||
seen that user or channel before. A call to ``GetMessagesRequest`` may
|
||||
still be neeed.
|
||||
|
||||
Via GetContactsRequest
|
||||
----------------------
|
||||
|
||||
The library will call this for you if you pass a phone number to
|
||||
``.get_entity``, but again, it can be done manually. If the user you
|
||||
want to talk to is a contact, you can use `GetContactsRequest`__:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.functions.contacts import GetContactsRequest
|
||||
from telethon.tl.types.contacts import Contacts
|
||||
|
||||
contacts = client(GetContactsRequest(0))
|
||||
if isinstance(contacts, Contacts):
|
||||
users = contacts.users
|
||||
contacts = contacts.contacts
|
||||
|
||||
__ https://lonamiwebs.github.io/Telethon/types/user.html
|
||||
__ https://lonamiwebs.github.io/Telethon/types/chat.html
|
||||
__ https://lonamiwebs.github.io/Telethon/constructors/channel.html
|
||||
__ https://t.me
|
||||
__ https://lonamiwebs.github.io/Telethon/types/peer.html
|
||||
__ https://lonamiwebs.github.io/Telethon/types/peer.html
|
||||
__ https://lonamiwebs.github.io/Telethon/constructors/message_fwd_header.html
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/messages/get_messages.html
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/contacts/get_contacts.html
|
||||
|
||||
|
||||
.. _retrieving-all-dialogs:
|
||||
|
||||
Retrieving all dialogs
|
||||
***********************
|
||||
|
||||
There are several ``offset_xyz=`` parameters that have no effect at all,
|
||||
but there's not much one can do since this is something the server should handle.
|
||||
Currently, the only way to get all dialogs
|
||||
(open chats, conversations, etc.) is by using the ``offset_date``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.functions.messages import GetDialogsRequest
|
||||
from telethon.tl.types import InputPeerEmpty
|
||||
from time import sleep
|
||||
|
||||
dialogs = []
|
||||
users = []
|
||||
chats = []
|
||||
|
||||
last_date = None
|
||||
chunk_size = 20
|
||||
while True:
|
||||
result = client(GetDialogsRequest(
|
||||
offset_date=last_date,
|
||||
offset_id=0,
|
||||
offset_peer=InputPeerEmpty(),
|
||||
limit=chunk_size
|
||||
))
|
||||
dialogs.extend(result.dialogs)
|
||||
users.extend(result.users)
|
||||
chats.extend(result.chats)
|
||||
if not result.messages:
|
||||
break
|
||||
last_date = min(msg.date for msg in result.messages)
|
||||
sleep(2)
|
||||
|
||||
|
||||
Joining a chat or channel
|
||||
*******************************
|
||||
|
||||
Note that `Chat`__\ s are normal groups, and `Channel`__\ s are a
|
||||
special form of `Chat`__\ s,
|
||||
which can also be super-groups if their ``megagroup`` member is
|
||||
``True``.
|
||||
|
||||
Joining a public channel
|
||||
------------------------
|
||||
|
||||
Once you have the :ref:`entity <retrieving-an-entity>`
|
||||
of the channel you want to join to, you can
|
||||
make use of the `JoinChannelRequest`__ to join such channel:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.functions.channels import JoinChannelRequest
|
||||
client(JoinChannelRequest(channel))
|
||||
|
||||
# In the same way, you can also leave such channel
|
||||
from telethon.tl.functions.channels import LeaveChannelRequest
|
||||
client(LeaveChannelRequest(input_channel))
|
||||
|
||||
For more on channels, check the `channels namespace`__.
|
||||
|
||||
Joining a private chat or channel
|
||||
---------------------------------
|
||||
|
||||
If all you have is a link like this one:
|
||||
``https://t.me/joinchat/AAAAAFFszQPyPEZ7wgxLtd``, you already have
|
||||
enough information to join! The part after the
|
||||
``https://t.me/joinchat/``, this is, ``AAAAAFFszQPyPEZ7wgxLtd`` on this
|
||||
example, is the ``hash`` of the chat or channel. Now you can use
|
||||
`ImportChatInviteRequest`__ as follows:
|
||||
|
||||
.. -block:: python
|
||||
|
||||
from telethon.tl.functions.messages import ImportChatInviteRequest
|
||||
updates = client(ImportChatInviteRequest('AAAAAEHbEkejzxUjAUCfYg'))
|
||||
|
||||
Adding someone else to such chat or channel
|
||||
-------------------------------------------
|
||||
|
||||
If you don’t want to add yourself, maybe because you’re already in, you
|
||||
can always add someone else with the `AddChatUserRequest`__, which
|
||||
use is very straightforward:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.functions.messages import AddChatUserRequest
|
||||
|
||||
client(AddChatUserRequest(
|
||||
chat_id,
|
||||
user_to_add,
|
||||
fwd_limit=10 # allow the user to see the 10 last messages
|
||||
))
|
||||
|
||||
Checking a link without joining
|
||||
-------------------------------
|
||||
|
||||
If you don’t need to join but rather check whether it’s a group or a
|
||||
channel, you can use the `CheckChatInviteRequest`__, which takes in
|
||||
the `hash`__ of said channel or group.
|
||||
|
||||
__ https://lonamiwebs.github.io/Telethon/constructors/chat.html
|
||||
__ https://lonamiwebs.github.io/Telethon/constructors/channel.html
|
||||
__ https://lonamiwebs.github.io/Telethon/types/chat.html
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/channels/join_channel.html
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/channels/index.html
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/messages/import_chat_invite.html
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/messages/add_chat_user.html
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/messages/check_chat_invite.html
|
||||
__ https://github.com/LonamiWebs/Telethon/wiki/Joining-a-chat-or-channel#joining-a-private-chat-or-channel
|
||||
|
||||
|
||||
Retrieving all chat members (channels too)
|
||||
******************************************
|
||||
|
||||
In order to get all the members from a mega-group or channel, you need
|
||||
to use `GetParticipantsRequest`__. As we can see it needs an
|
||||
`InputChannel`__, (passing the mega-group or channel you’re going to
|
||||
use will work), and a mandatory `ChannelParticipantsFilter`__. The
|
||||
closest thing to “no filter” is to simply use
|
||||
`ChannelParticipantsSearch`__ with an empty ``'q'`` string.
|
||||
|
||||
If we want to get *all* the members, we need to use a moving offset and
|
||||
a fixed limit:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.functions.channels import GetParticipantsRequest
|
||||
from telethon.tl.types import ChannelParticipantsSearch
|
||||
from time import sleep
|
||||
|
||||
offset = 0
|
||||
limit = 100
|
||||
all_participants = []
|
||||
|
||||
while True:
|
||||
participants = client.invoke(GetParticipantsRequest(
|
||||
channel, ChannelParticipantsSearch(''), offset, limit
|
||||
))
|
||||
if not participants.users:
|
||||
break
|
||||
all_participants.extend(participants.users)
|
||||
offset += len(participants.users)
|
||||
# sleep(1) # This line seems to be optional, no guarantees!
|
||||
|
||||
Note that ``GetParticipantsRequest`` returns `ChannelParticipants`__,
|
||||
which may have more information you need (like the role of the
|
||||
participants, total count of members, etc.)
|
||||
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/channels/get_participants.html
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/channels/get_participants.html
|
||||
__ https://lonamiwebs.github.io/Telethon/types/channel_participants_filter.html
|
||||
__ https://lonamiwebs.github.io/Telethon/constructors/channel_participants_search.html
|
||||
__ https://lonamiwebs.github.io/Telethon/constructors/channels/channel_participants.html
|
||||
|
||||
|
||||
Recent Actions
|
||||
********************
|
||||
|
||||
“Recent actions” is simply the name official applications have given to
|
||||
the “admin log”. Simply use `GetAdminLogRequest`__ for that, and
|
||||
you’ll get AdminLogResults.events in return which in turn has the final
|
||||
`.action`__.
|
||||
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/channels/get_admin_log.html
|
||||
__ https://lonamiwebs.github.io/Telethon/types/channel_admin_log_event_action.html
|
||||
|
||||
|
||||
Increasing View Count in a Channel
|
||||
****************************************
|
||||
|
||||
It has been asked `quite`__ `a few`__ `times`__ (really, `many`__), and
|
||||
while I don’t understand why so many people ask this, the solution is to
|
||||
use `GetMessagesViewsRequest`__, setting ``increment=True``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
||||
# Obtain `channel' through dialogs or through client.get_entity() or anyhow.
|
||||
# Obtain `msg_ids' through `.get_message_history()` or anyhow. Must be a list.
|
||||
|
||||
client(GetMessagesViewsRequest(
|
||||
peer=channel,
|
||||
id=msg_ids,
|
||||
increment=True
|
||||
))
|
||||
|
||||
__ https://github.com/LonamiWebs/Telethon/issues/233
|
||||
__ https://github.com/LonamiWebs/Telethon/issues/305
|
||||
__ https://github.com/LonamiWebs/Telethon/issues/409
|
||||
__ https://github.com/LonamiWebs/Telethon/issues/447
|
||||
__ https://lonamiwebs.github.io/Telethon/methods/messages/get_messages_views.html
|
103
readthedocs/extra/advanced-usage/working-with-messages.rst
Normal file
103
readthedocs/extra/advanced-usage/working-with-messages.rst
Normal 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
|
48
readthedocs/extra/advanced.rst
Normal file
48
readthedocs/extra/advanced.rst
Normal 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
|
117
readthedocs/extra/basic/accessing-the-full-api.rst
Normal file
117
readthedocs/extra/basic/accessing-the-full-api.rst
Normal file
|
@ -0,0 +1,117 @@
|
|||
.. _accessing-the-full-api:
|
||||
|
||||
==========================
|
||||
Accessing the Full API
|
||||
==========================
|
||||
|
||||
The ``TelegramClient`` doesn’t offer a method for every single request
|
||||
the Telegram API supports. However, it’s very simple to ``.invoke()``
|
||||
any request. Whenever you need something, don’t forget to `check the
|
||||
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 that’s the reason for this distinction.
|
||||
|
||||
Say ``client.send_message()`` didn’t exist, we could use the `search`__
|
||||
to look for “message”. There we would find `SendMessageRequest`__,
|
||||
which we can work with.
|
||||
|
||||
Every request is a Python class, and has the parameters needed for you
|
||||
to invoke it. You can also call ``help(request)`` for information on
|
||||
what input parameters it takes. Remember to “Copy import to the
|
||||
clipboard”, or your script won’t be aware of this class! Now we have:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telethon.tl.functions.messages import SendMessageRequest
|
||||
|
||||
If you’re 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 you’re going to invoke an API method, most require you to pass an
|
||||
``InputUser``, ``InputChat``, or so on, this is why using
|
||||
``.get_input_entity()`` is more straightforward (and sometimes
|
||||
immediate, if you know the ID of the user for instance). If you also
|
||||
need to have information about the whole user, use ``.get_entity()``
|
||||
instead:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
entity = client.get_entity('someone')
|
||||
|
||||
In the later case, when you use the entity, the library will cast it to
|
||||
its “input” version for you. If you already have the complete user and
|
||||
want to cache its input version so the library doesn’t have to do this
|
||||
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
|
76
readthedocs/extra/basic/creating-a-client.rst
Normal file
76
readthedocs/extra/basic/creating-a-client.rst
Normal 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
|
54
readthedocs/extra/basic/getting-started.rst
Normal file
54
readthedocs/extra/basic/getting-started.rst
Normal 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])
|
||||
|
71
readthedocs/extra/basic/installation.rst
Normal file
71
readthedocs/extra/basic/installation.rst
Normal 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
|
55
readthedocs/extra/basic/sending-requests.rst
Normal file
55
readthedocs/extra/basic/sending-requests.rst
Normal 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.
|
48
readthedocs/extra/basic/sessions.rst
Normal file
48
readthedocs/extra/basic/sessions.rst
Normal 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 you’ve
|
||||
seen, so that you can get information about an user or channel by just
|
||||
their ID. Telegram will **not** send their ``access_hash`` required to
|
||||
retrieve more information about them, if it thinks you have already seem
|
||||
them. For this reason, the library needs to store this information
|
||||
offline.
|
||||
|
||||
The library will by default too save all the entities (users with their
|
||||
name, username, chats and so on) **in memory**, not to disk, so that you
|
||||
can quickly access them by username or phone number. This can be
|
||||
disabled too. Run ``help(client.session.entities)`` to see the available
|
||||
methods (or ``help(EntityDatabase)``).
|
||||
|
||||
If you’re not going to work without updates, or don’t need to cache the
|
||||
``access_hash`` associated with the entities’ ID, you can disable this
|
||||
by setting ``client.session.save_entities = False``.
|
||||
|
||||
If you don’t want to save the files as JSON, you can also create your
|
||||
custom ``Session`` subclass and override the ``.save()`` and ``.load()``
|
||||
methods. For example, you could save it on a database:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class DatabaseSession(Session):
|
||||
def save():
|
||||
# serialize relevant data to the database
|
||||
|
||||
def load():
|
||||
# load relevant data to the database
|
||||
|
||||
You should read the ``session.py`` source file to know what “relevant
|
||||
data” you need to keep track of.
|
135
readthedocs/extra/basic/working-with-updates.rst
Normal file
135
readthedocs/extra/basic/working-with-updates.rst
Normal 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
|
|
@ -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
|
24
readthedocs/extra/troubleshooting/enable-logging.rst
Normal file
24
readthedocs/extra/troubleshooting/enable-logging.rst
Normal 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
|
27
readthedocs/extra/troubleshooting/rpc-errors.rst
Normal file
27
readthedocs/extra/troubleshooting/rpc-errors.rst
Normal file
|
@ -0,0 +1,27 @@
|
|||
==========
|
||||
RPC Errors
|
||||
==========
|
||||
|
||||
RPC stands for Remote Procedure Call, and when Telethon raises an
|
||||
``RPCError``, it’s most likely because you have invoked some of the API
|
||||
methods incorrectly (wrong parameters, wrong permissions, or even
|
||||
something went wrong on Telegram’s 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 don’t 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 you’re invoking ``Request``\ ’s!
|
||||
|
||||
If the error is not recognised, it will only be an ``RPCError``.
|
65
readthedocs/index.rst
Normal file
65
readthedocs/index.rst
Normal 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
36
readthedocs/make.bat
Normal 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
7
readthedocs/modules.rst
Normal file
|
@ -0,0 +1,7 @@
|
|||
telethon
|
||||
========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
telethon
|
1
readthedocs/requirements.txt
Normal file
1
readthedocs/requirements.txt
Normal file
|
@ -0,0 +1 @@
|
|||
telethon
|
61
readthedocs/telethon.crypto.rst
Normal file
61
readthedocs/telethon.crypto.rst
Normal 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:
|
||||
|
||||
|
21
readthedocs/telethon.errors.rst
Normal file
21
readthedocs/telethon.errors.rst
Normal 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:
|
||||
|
||||
|
29
readthedocs/telethon.extensions.rst
Normal file
29
readthedocs/telethon.extensions.rst
Normal 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:
|
||||
|
||||
|
37
readthedocs/telethon.network.rst
Normal file
37
readthedocs/telethon.network.rst
Normal 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
89
readthedocs/telethon.rst
Normal 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:
|
12
readthedocs/telethon.tl.custom.rst
Normal file
12
readthedocs/telethon.tl.custom.rst
Normal file
|
@ -0,0 +1,12 @@
|
|||
telethon\.tl\.custom package
|
||||
============================
|
||||
|
||||
|
||||
telethon\.tl\.custom\.draft module
|
||||
----------------------------------
|
||||
|
||||
.. automodule:: telethon.tl.custom.draft
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
57
readthedocs/telethon.tl.rst
Normal file
57
readthedocs/telethon.tl.rst
Normal 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:
|
||||
|
29
setup.py
29
setup.py
|
@ -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,
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
"""
|
||||
This module contains several utilities regarding cryptographic purposes,
|
||||
such as the AES IGE mode used by Telegram, the authorization key bound with
|
||||
their data centers, and so on.
|
||||
"""
|
||||
from .aes import AES
|
||||
from .aes_ctr import AESModeCTR
|
||||
from .auth_key import AuthKey
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
"""
|
||||
AES IGE implementation in Python. This module may use libssl if available.
|
||||
"""
|
||||
import os
|
||||
import pyaes
|
||||
from . import libssl
|
||||
|
@ -9,10 +12,15 @@ if libssl.AES is not None:
|
|||
else:
|
||||
# Fallback to a pure Python implementation
|
||||
class AES:
|
||||
"""
|
||||
Class that servers as an interface to encrypt and decrypt
|
||||
text through the AES IGE mode.
|
||||
"""
|
||||
@staticmethod
|
||||
def decrypt_ige(cipher_text, key, iv):
|
||||
"""Decrypts the given text in 16-bytes blocks by using the
|
||||
given key and 32-bytes initialization vector
|
||||
"""
|
||||
Decrypts the given text in 16-bytes blocks by using the
|
||||
given key and 32-bytes initialization vector.
|
||||
"""
|
||||
iv1 = iv[:len(iv) // 2]
|
||||
iv2 = iv[len(iv) // 2:]
|
||||
|
@ -42,8 +50,9 @@ else:
|
|||
|
||||
@staticmethod
|
||||
def encrypt_ige(plain_text, key, iv):
|
||||
"""Encrypts the given text in 16-bytes blocks by using the
|
||||
given key and 32-bytes initialization vector
|
||||
"""
|
||||
Encrypts the given text in 16-bytes blocks by using the
|
||||
given key and 32-bytes initialization vector.
|
||||
"""
|
||||
|
||||
# Add random padding iff it's not evenly divisible by 16 already
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
"""
|
||||
This module holds the AESModeCTR wrapper class.
|
||||
"""
|
||||
import pyaes
|
||||
|
||||
|
||||
|
@ -6,6 +9,12 @@ class AESModeCTR:
|
|||
# TODO Maybe make a pull request to pyaes to support iv on CTR
|
||||
|
||||
def __init__(self, key, iv):
|
||||
"""
|
||||
Initializes the AES CTR mode with the given key/iv pair.
|
||||
|
||||
:param key: the key to be used as bytes.
|
||||
:param iv: the bytes initialization vector. Must have a length of 16.
|
||||
"""
|
||||
# TODO Use libssl if available
|
||||
assert isinstance(key, bytes)
|
||||
self._aes = pyaes.AESModeOfOperationCTR(key)
|
||||
|
@ -15,7 +24,19 @@ class AESModeCTR:
|
|||
self._aes._counter._counter = list(iv)
|
||||
|
||||
def encrypt(self, data):
|
||||
"""
|
||||
Encrypts the given plain text through AES CTR.
|
||||
|
||||
:param data: the plain text to be encrypted.
|
||||
:return: the encrypted cipher text.
|
||||
"""
|
||||
return self._aes.encrypt(data)
|
||||
|
||||
def decrypt(self, data):
|
||||
"""
|
||||
Decrypts the given cipher text through AES CTR
|
||||
|
||||
:param data: the cipher text to be decrypted.
|
||||
:return: the decrypted plain text.
|
||||
"""
|
||||
return self._aes.decrypt(data)
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
"""
|
||||
This module holds the AuthKey class.
|
||||
"""
|
||||
import struct
|
||||
from hashlib import sha1
|
||||
|
||||
|
@ -6,7 +9,16 @@ from ..extensions import BinaryReader
|
|||
|
||||
|
||||
class AuthKey:
|
||||
"""
|
||||
Represents an authorization key, used to encrypt and decrypt
|
||||
messages sent to Telegram's data centers.
|
||||
"""
|
||||
def __init__(self, data):
|
||||
"""
|
||||
Initializes a new authorization key.
|
||||
|
||||
:param data: the data in bytes that represent this auth key.
|
||||
"""
|
||||
self.key = data
|
||||
|
||||
with BinaryReader(sha1(self.key).digest()) as reader:
|
||||
|
@ -15,8 +27,12 @@ class AuthKey:
|
|||
self.key_id = reader.read_long(signed=False)
|
||||
|
||||
def calc_new_nonce_hash(self, new_nonce, number):
|
||||
"""Calculates the new nonce hash based on
|
||||
the current class fields' values
|
||||
"""
|
||||
Calculates the new nonce hash based on the current attributes.
|
||||
|
||||
:param new_nonce: the new nonce to be hashed.
|
||||
:param number: number to prepend before the hash.
|
||||
:return: the hash for the given new nonce.
|
||||
"""
|
||||
new_nonce = new_nonce.to_bytes(32, 'little', signed=True)
|
||||
data = new_nonce + struct.pack('<BQ', number, self.aux_hash)
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
"""
|
||||
This module holds the CdnDecrypter utility class.
|
||||
"""
|
||||
from hashlib import sha256
|
||||
|
||||
from ..tl import Session
|
||||
from ..tl.functions.upload import GetCdnFileRequest, ReuploadCdnFileRequest
|
||||
from ..tl.types.upload import CdnFileReuploadNeeded, CdnFile
|
||||
from ..crypto import AESModeCTR
|
||||
|
@ -8,11 +10,20 @@ from ..errors import CdnFileTamperedError
|
|||
|
||||
|
||||
class CdnDecrypter:
|
||||
"""Used when downloading a file results in a 'FileCdnRedirect' to
|
||||
both prepare the redirect, decrypt the file as it downloads, and
|
||||
ensure the file hasn't been tampered. https://core.telegram.org/cdn
|
||||
"""
|
||||
Used when downloading a file results in a 'FileCdnRedirect' to
|
||||
both prepare the redirect, decrypt the file as it downloads, and
|
||||
ensure the file hasn't been tampered. https://core.telegram.org/cdn
|
||||
"""
|
||||
def __init__(self, cdn_client, file_token, cdn_aes, cdn_file_hashes):
|
||||
"""
|
||||
Initializes the CDN decrypter.
|
||||
|
||||
:param cdn_client: a client connected to a CDN.
|
||||
:param file_token: the token of the file to be used.
|
||||
:param cdn_aes: the AES CTR used to decrypt the file.
|
||||
:param cdn_file_hashes: the hashes the decrypted file must match.
|
||||
"""
|
||||
self.client = cdn_client
|
||||
self.file_token = file_token
|
||||
self.cdn_aes = cdn_aes
|
||||
|
@ -20,10 +31,13 @@ class CdnDecrypter:
|
|||
|
||||
@staticmethod
|
||||
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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
"""
|
||||
This module holds an AES IGE class, if libssl is available on the system.
|
||||
"""
|
||||
import os
|
||||
import ctypes
|
||||
from ctypes.util import find_library
|
||||
|
@ -35,14 +38,23 @@ else:
|
|||
AES_DECRYPT = ctypes.c_int(0)
|
||||
|
||||
class AES_KEY(ctypes.Structure):
|
||||
"""Helper class representing an AES key"""
|
||||
_fields_ = [
|
||||
('rd_key', ctypes.c_uint32 * (4*(AES_MAXNR + 1))),
|
||||
('rounds', ctypes.c_uint),
|
||||
]
|
||||
|
||||
class AES:
|
||||
"""
|
||||
Class that servers as an interface to encrypt and decrypt
|
||||
text through the AES IGE mode, using the system's libssl.
|
||||
"""
|
||||
@staticmethod
|
||||
def decrypt_ige(cipher_text, key, iv):
|
||||
"""
|
||||
Decrypts the given text in 16-bytes blocks by using the
|
||||
given key and 32-bytes initialization vector.
|
||||
"""
|
||||
aeskey = AES_KEY()
|
||||
ckey = (ctypes.c_ubyte * len(key))(*key)
|
||||
cklen = ctypes.c_int(len(key)*8)
|
||||
|
@ -65,6 +77,10 @@ else:
|
|||
|
||||
@staticmethod
|
||||
def encrypt_ige(plain_text, key, iv):
|
||||
"""
|
||||
Encrypts the given text in 16-bytes blocks by using the
|
||||
given key and 32-bytes initialization vector.
|
||||
"""
|
||||
# Add random padding iff it's not evenly divisible by 16 already
|
||||
if len(plain_text) % 16 != 0:
|
||||
padding_count = 16 - len(plain_text) % 16
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
"""
|
||||
This module holds several utilities regarding RSA and server fingerprints.
|
||||
"""
|
||||
import os
|
||||
import struct
|
||||
from hashlib import sha1
|
||||
|
@ -32,8 +35,11 @@ def get_byte_array(integer):
|
|||
|
||||
|
||||
def _compute_fingerprint(key):
|
||||
"""For a given Crypto.RSA key, computes its 8-bytes-long fingerprint
|
||||
in the same way that Telegram does.
|
||||
"""
|
||||
Given a RSA key, computes its fingerprint like Telegram does.
|
||||
|
||||
:param key: the Crypto.RSA key.
|
||||
:return: its 8-bytes-long fingerprint.
|
||||
"""
|
||||
n = TLObject.serialize_bytes(get_byte_array(key.n))
|
||||
e = TLObject.serialize_bytes(get_byte_array(key.e))
|
||||
|
@ -49,8 +55,14 @@ def add_key(pub):
|
|||
|
||||
|
||||
def encrypt(fingerprint, data):
|
||||
"""Given the fingerprint of a previously added RSA key, encrypt its data
|
||||
in the way Telegram requires us to do so (sha1(data) + data + padding)
|
||||
"""
|
||||
Encrypts the given data known the fingerprint to be used
|
||||
in the way Telegram requires us to do so (sha1(data) + data + padding)
|
||||
|
||||
:param fingerprint: the fingerprint of the RSA key.
|
||||
:param data: the data to be encrypted.
|
||||
:return:
|
||||
the cipher text, or None if no key matching this fingerprint is found.
|
||||
"""
|
||||
global _server_keys
|
||||
key = _server_keys.get(fingerprint, None)
|
||||
|
|
|
@ -1,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,
|
||||
|
|
|
@ -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.'
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
class RPCError(Exception):
|
||||
"""Base class for all Remote Procedure Call errors."""
|
||||
code = None
|
||||
message = None
|
||||
|
||||
|
||||
class InvalidDCError(RPCError):
|
||||
"""
|
||||
The request must be repeated, but directed to a different data center.
|
||||
The request must be repeated, but directed to a different data center.
|
||||
"""
|
||||
code = 303
|
||||
message = 'ERROR_SEE_OTHER'
|
||||
|
@ -13,9 +14,9 @@ class InvalidDCError(RPCError):
|
|||
|
||||
class BadRequestError(RPCError):
|
||||
"""
|
||||
The query contains errors. In the event that a request was created
|
||||
using a form and contains user generated data, the user should be
|
||||
notified that the data must be corrected before the query is repeated.
|
||||
The query contains errors. In the event that a request was created
|
||||
using a form and contains user generated data, the user should be
|
||||
notified that the data must be corrected before the query is repeated.
|
||||
"""
|
||||
code = 400
|
||||
message = 'BAD_REQUEST'
|
||||
|
@ -23,8 +24,8 @@ class BadRequestError(RPCError):
|
|||
|
||||
class UnauthorizedError(RPCError):
|
||||
"""
|
||||
There was an unauthorized attempt to use functionality available only
|
||||
to authorized users.
|
||||
There was an unauthorized attempt to use functionality available only
|
||||
to authorized users.
|
||||
"""
|
||||
code = 401
|
||||
message = 'UNAUTHORIZED'
|
||||
|
@ -32,8 +33,8 @@ class UnauthorizedError(RPCError):
|
|||
|
||||
class ForbiddenError(RPCError):
|
||||
"""
|
||||
Privacy violation. For example, an attempt to write a message to
|
||||
someone who has blacklisted the current user.
|
||||
Privacy violation. For example, an attempt to write a message to
|
||||
someone who has blacklisted the current user.
|
||||
"""
|
||||
code = 403
|
||||
message = 'FORBIDDEN'
|
||||
|
@ -45,7 +46,7 @@ class ForbiddenError(RPCError):
|
|||
|
||||
class NotFoundError(RPCError):
|
||||
"""
|
||||
An attempt to invoke a non-existent object, such as a method.
|
||||
An attempt to invoke a non-existent object, such as a method.
|
||||
"""
|
||||
code = 404
|
||||
message = 'NOT_FOUND'
|
||||
|
@ -57,10 +58,10 @@ class NotFoundError(RPCError):
|
|||
|
||||
class FloodError(RPCError):
|
||||
"""
|
||||
The maximum allowed number of attempts to invoke the given method
|
||||
with the given input parameters has been exceeded. For example, in an
|
||||
attempt to request a large number of text messages (SMS) for the same
|
||||
phone number.
|
||||
The maximum allowed number of attempts to invoke the given method
|
||||
with the given input parameters has been exceeded. For example, in an
|
||||
attempt to request a large number of text messages (SMS) for the same
|
||||
phone number.
|
||||
"""
|
||||
code = 420
|
||||
message = 'FLOOD'
|
||||
|
@ -68,9 +69,9 @@ class FloodError(RPCError):
|
|||
|
||||
class ServerError(RPCError):
|
||||
"""
|
||||
An internal server error occurred while a request was being processed
|
||||
for example, there was a disruption while accessing a database or file
|
||||
storage.
|
||||
An internal server error occurred while a request was being processed
|
||||
for example, there was a disruption while accessing a database or file
|
||||
storage.
|
||||
"""
|
||||
code = 500
|
||||
message = 'INTERNAL'
|
||||
|
@ -81,38 +82,42 @@ class ServerError(RPCError):
|
|||
|
||||
|
||||
class BadMessageError(Exception):
|
||||
"""Occurs when handling a bad_message_notification"""
|
||||
"""Occurs when handling a bad_message_notification."""
|
||||
ErrorMessages = {
|
||||
16:
|
||||
'msg_id too low (most likely, client time is wrong it would be worthwhile to '
|
||||
'synchronize it using msg_id notifications and re-send the original message '
|
||||
'with the "correct" msg_id or wrap it in a container with a new msg_id if the '
|
||||
'original message had waited too long on the client to be transmitted).',
|
||||
'msg_id too low (most likely, client time is wrong it would be '
|
||||
'worthwhile to synchronize it using msg_id notifications and re-send '
|
||||
'the original message with the "correct" msg_id or wrap it in a '
|
||||
'container with a new msg_id if the original message had waited too '
|
||||
'long on the client to be transmitted).',
|
||||
17:
|
||||
'msg_id too high (similar to the previous case, the client time has to be '
|
||||
'synchronized, and the message re-sent with the correct msg_id).',
|
||||
'msg_id too high (similar to the previous case, the client time has '
|
||||
'to be synchronized, and the message re-sent with the correct msg_id).',
|
||||
18:
|
||||
'Incorrect two lower order msg_id bits (the server expects client message msg_id '
|
||||
'to be divisible by 4).',
|
||||
'Incorrect two lower order msg_id bits (the server expects client '
|
||||
'message msg_id to be divisible by 4).',
|
||||
19:
|
||||
'Container msg_id is the same as msg_id of a previously received message '
|
||||
'(this must never happen).',
|
||||
'Container msg_id is the same as msg_id of a previously received '
|
||||
'message (this must never happen).',
|
||||
20:
|
||||
'Message too old, and it cannot be verified whether the server has received a '
|
||||
'message with this msg_id or not.',
|
||||
'Message too old, and it cannot be verified whether the server has '
|
||||
'received a message with this msg_id or not.',
|
||||
32:
|
||||
'msg_seqno too low (the server has already received a message with a lower '
|
||||
'msg_id but with either a higher or an equal and odd seqno).',
|
||||
'msg_seqno too low (the server has already received a message with a '
|
||||
'lower msg_id but with either a higher or an equal and odd seqno).',
|
||||
33:
|
||||
'msg_seqno too high (similarly, there is a message with a higher msg_id but with '
|
||||
'either a lower or an equal and odd seqno).',
|
||||
'msg_seqno too high (similarly, there is a message with a higher '
|
||||
'msg_id but with either a lower or an equal and odd seqno).',
|
||||
34:
|
||||
'An even msg_seqno expected (irrelevant message), but odd received.',
|
||||
35: 'Odd msg_seqno expected (relevant message), but even received.',
|
||||
35:
|
||||
'Odd msg_seqno expected (relevant message), but even received.',
|
||||
48:
|
||||
'Incorrect server salt (in this case, the bad_server_salt response is received with '
|
||||
'the correct salt, and the message is to be re-sent with it).',
|
||||
64: 'Invalid container.'
|
||||
'Incorrect server salt (in this case, the bad_server_salt response '
|
||||
'is received with the correct salt, and the message is to be re-sent '
|
||||
'with it).',
|
||||
64:
|
||||
'Invalid container.'
|
||||
}
|
||||
|
||||
def __init__(self, code):
|
||||
|
|
|
@ -1,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)
|
||||
|
||||
|
|
208
telethon/extensions/markdown.py
Normal file
208
telethon/extensions/markdown.py
Normal 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]
|
|
@ -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.')
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
"""
|
||||
This module contains several classes regarding network, low level connection
|
||||
with Telegram's servers and the protocol used (TCP full, abridged, etc.).
|
||||
"""
|
||||
from .mtproto_plain_sender import MtProtoPlainSender
|
||||
from .authenticator import do_authentication
|
||||
from .mtproto_sender import MtProtoSender
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
"""
|
||||
This module contains several functions that authenticate the client machine
|
||||
with Telegram's servers, effectively creating an authorization key.
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
from hashlib import sha1
|
||||
|
@ -18,6 +22,14 @@ from ..tl.functions import (
|
|||
|
||||
|
||||
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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
from .draft import Draft
|
||||
from .dialog import Dialog
|
||||
|
|
37
telethon/tl/custom/dialog.py
Normal file
37
telethon/tl/custom/dialog.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
from . import Draft
|
||||
from ... import utils
|
||||
|
||||
|
||||
class Dialog:
|
||||
"""
|
||||
Custom class that encapsulates a dialog (an open "conversation" with
|
||||
someone, a group or a channel) providing an abstraction to easily
|
||||
access the input version/normal entity/message etc. The library will
|
||||
return instances of this class when calling `client.get_dialogs()`.
|
||||
"""
|
||||
def __init__(self, client, dialog, entities, messages):
|
||||
# Both entities and messages being dicts {ID: item}
|
||||
self._client = client
|
||||
self.dialog = dialog
|
||||
self.pinned = bool(dialog.pinned)
|
||||
self.message = messages.get(dialog.top_message, None)
|
||||
self.date = getattr(self.message, 'date', None)
|
||||
|
||||
self.entity = entities[utils.get_peer_id(dialog.peer, 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)
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
3
telethon/version.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Versions should comply with PEP440.
|
||||
# This line is parsed in setup.py:
|
||||
__version__ = '0.15.5'
|
BIN
telethon_examples/anytime.png
Normal file
BIN
telethon_examples/anytime.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
|
@ -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
|
||||
))
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user