diff --git a/.gitignore b/.gitignore index 156d23e3..f2090e85 100755 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ -.idea +# Docs +_build/ + +# Generated code telethon/tl/functions/ telethon/tl/types/ telethon/tl/all_tlobjects.py diff --git a/readthedocs/Makefile b/readthedocs/Makefile new file mode 100644 index 00000000..fd6e0d0a --- /dev/null +++ b/readthedocs/Makefile @@ -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) \ No newline at end of file diff --git a/readthedocs/conf.py b/readthedocs/conf.py new file mode 100644 index 00000000..18ff1a17 --- /dev/null +++ b/readthedocs/conf.py @@ -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'), +] + + + diff --git a/readthedocs/extra/advanced-usage/bots.rst b/readthedocs/extra/advanced-usage/bots.rst new file mode 100644 index 00000000..091eada1 --- /dev/null +++ b/readthedocs/extra/advanced-usage/bots.rst @@ -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 \ No newline at end of file diff --git a/readthedocs/extra/advanced-usage/signing-in.rst b/readthedocs/extra/advanced-usage/signing-in.rst new file mode 100644 index 00000000..08f4fe3d --- /dev/null +++ b/readthedocs/extra/advanced-usage/signing-in.rst @@ -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 `_ for the tip! + diff --git a/readthedocs/extra/advanced-usage/users-and-chats.rst b/readthedocs/extra/advanced-usage/users-and-chats.rst new file mode 100644 index 00000000..a48a2857 --- /dev/null +++ b/readthedocs/extra/advanced-usage/users-and-chats.rst @@ -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 ` +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 \ No newline at end of file diff --git a/readthedocs/extra/advanced-usage/working-with-messages.rst b/readthedocs/extra/advanced-usage/working-with-messages.rst new file mode 100644 index 00000000..2c141406 --- /dev/null +++ b/readthedocs/extra/advanced-usage/working-with-messages.rst @@ -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 diff --git a/readthedocs/extra/advanced.rst b/readthedocs/extra/advanced.rst new file mode 100644 index 00000000..4433116d --- /dev/null +++ b/readthedocs/extra/advanced.rst @@ -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 diff --git a/readthedocs/extra/basic/accessing-the-full-api.rst b/readthedocs/extra/basic/accessing-the-full-api.rst new file mode 100644 index 00000000..ab6682db --- /dev/null +++ b/readthedocs/extra/basic/accessing-the-full-api.rst @@ -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 \ No newline at end of file diff --git a/readthedocs/extra/basic/creating-a-client.rst b/readthedocs/extra/basic/creating-a-client.rst new file mode 100644 index 00000000..997386db --- /dev/null +++ b/readthedocs/extra/basic/creating-a-client.rst @@ -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 `_ 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 \ No newline at end of file diff --git a/readthedocs/extra/basic/getting-started.rst b/readthedocs/extra/basic/getting-started.rst new file mode 100644 index 00000000..bad3ea30 --- /dev/null +++ b/readthedocs/extra/basic/getting-started.rst @@ -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]) + diff --git a/readthedocs/extra/basic/installation.rst b/readthedocs/extra/basic/installation.rst new file mode 100644 index 00000000..ecad699b --- /dev/null +++ b/readthedocs/extra/basic/installation.rst @@ -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 \ No newline at end of file diff --git a/readthedocs/extra/basic/sending-requests.rst b/readthedocs/extra/basic/sending-requests.rst new file mode 100644 index 00000000..160e2259 --- /dev/null +++ b/readthedocs/extra/basic/sending-requests.rst @@ -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. diff --git a/readthedocs/extra/basic/sessions.rst b/readthedocs/extra/basic/sessions.rst new file mode 100644 index 00000000..f55d9703 --- /dev/null +++ b/readthedocs/extra/basic/sessions.rst @@ -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. diff --git a/readthedocs/extra/basic/working-with-updates.rst b/readthedocs/extra/basic/working-with-updates.rst new file mode 100644 index 00000000..c5d9e919 --- /dev/null +++ b/readthedocs/extra/basic/working-with-updates.rst @@ -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 \ No newline at end of file diff --git a/readthedocs/extra/troubleshooting/deleted-limited-or-deactivated-accounts.rst b/readthedocs/extra/troubleshooting/deleted-limited-or-deactivated-accounts.rst new file mode 100644 index 00000000..1ad3da19 --- /dev/null +++ b/readthedocs/extra/troubleshooting/deleted-limited-or-deactivated-accounts.rst @@ -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 \ No newline at end of file diff --git a/readthedocs/extra/troubleshooting/enable-logging.rst b/readthedocs/extra/troubleshooting/enable-logging.rst new file mode 100644 index 00000000..a6d45d00 --- /dev/null +++ b/readthedocs/extra/troubleshooting/enable-logging.rst @@ -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 \ No newline at end of file diff --git a/readthedocs/extra/troubleshooting/rpc-errors.rst b/readthedocs/extra/troubleshooting/rpc-errors.rst new file mode 100644 index 00000000..6e8a59f0 --- /dev/null +++ b/readthedocs/extra/troubleshooting/rpc-errors.rst @@ -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``. \ No newline at end of file diff --git a/readthedocs/index.rst b/readthedocs/index.rst new file mode 100644 index 00000000..b5c77e6b --- /dev/null +++ b/readthedocs/index.rst @@ -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 `_. + + + +.. _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` diff --git a/readthedocs/make.bat b/readthedocs/make.bat new file mode 100644 index 00000000..f51f7234 --- /dev/null +++ b/readthedocs/make.bat @@ -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 diff --git a/readthedocs/modules.rst b/readthedocs/modules.rst new file mode 100644 index 00000000..f710574a --- /dev/null +++ b/readthedocs/modules.rst @@ -0,0 +1,7 @@ +telethon +======== + +.. toctree:: + :maxdepth: 3 + + telethon diff --git a/readthedocs/requirements.txt b/readthedocs/requirements.txt new file mode 100644 index 00000000..97c7493d --- /dev/null +++ b/readthedocs/requirements.txt @@ -0,0 +1 @@ +telethon \ No newline at end of file diff --git a/readthedocs/telethon.crypto.rst b/readthedocs/telethon.crypto.rst new file mode 100644 index 00000000..3c11416d --- /dev/null +++ b/readthedocs/telethon.crypto.rst @@ -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: + + diff --git a/readthedocs/telethon.errors.rst b/readthedocs/telethon.errors.rst new file mode 100644 index 00000000..2e94fe33 --- /dev/null +++ b/readthedocs/telethon.errors.rst @@ -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: + + diff --git a/readthedocs/telethon.extensions.rst b/readthedocs/telethon.extensions.rst new file mode 100644 index 00000000..578728b5 --- /dev/null +++ b/readthedocs/telethon.extensions.rst @@ -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: + + diff --git a/readthedocs/telethon.network.rst b/readthedocs/telethon.network.rst new file mode 100644 index 00000000..3600e985 --- /dev/null +++ b/readthedocs/telethon.network.rst @@ -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: + + diff --git a/readthedocs/telethon.rst b/readthedocs/telethon.rst new file mode 100644 index 00000000..2d3c269c --- /dev/null +++ b/readthedocs/telethon.rst @@ -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: diff --git a/readthedocs/telethon.tl.custom.rst b/readthedocs/telethon.tl.custom.rst new file mode 100644 index 00000000..a1290869 --- /dev/null +++ b/readthedocs/telethon.tl.custom.rst @@ -0,0 +1,12 @@ +telethon\.tl\.custom package +============================ + + +telethon\.tl\.custom\.draft module +---------------------------------- + +.. automodule:: telethon.tl.custom.draft + :members: + :undoc-members: + :show-inheritance: + diff --git a/readthedocs/telethon.tl.rst b/readthedocs/telethon.tl.rst new file mode 100644 index 00000000..6fbb1f00 --- /dev/null +++ b/readthedocs/telethon.tl.rst @@ -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: + diff --git a/setup.py b/setup.py index 2058924f..0c531d70 100755 --- a/setup.py +++ b/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, diff --git a/telethon/__init__.py b/telethon/__init__.py index c8593168..2f984bf1 100644 --- a/telethon/__init__.py +++ b/telethon/__init__.py @@ -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()) diff --git a/telethon/crypto/__init__.py b/telethon/crypto/__init__.py index d151a96c..aa470adf 100644 --- a/telethon/crypto/__init__.py +++ b/telethon/crypto/__init__.py @@ -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 diff --git a/telethon/crypto/aes.py b/telethon/crypto/aes.py index c09add56..191cde15 100644 --- a/telethon/crypto/aes.py +++ b/telethon/crypto/aes.py @@ -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 diff --git a/telethon/crypto/aes_ctr.py b/telethon/crypto/aes_ctr.py index 7bd7b79a..34422904 100644 --- a/telethon/crypto/aes_ctr.py +++ b/telethon/crypto/aes_ctr.py @@ -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) diff --git a/telethon/crypto/auth_key.py b/telethon/crypto/auth_key.py index 17a7f8ca..679e62ff 100644 --- a/telethon/crypto/auth_key.py +++ b/telethon/crypto/auth_key.py @@ -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('= 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 diff --git a/telethon/crypto/libssl.py b/telethon/crypto/libssl.py index f9917d71..b4735112 100644 --- a/telethon/crypto/libssl.py +++ b/telethon/crypto/libssl.py @@ -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 diff --git a/telethon/crypto/rsa.py b/telethon/crypto/rsa.py index 9abefc55..0303587b 100644 --- a/telethon/crypto/rsa.py +++ b/telethon/crypto/rsa.py @@ -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) diff --git a/telethon/errors/__init__.py b/telethon/errors/__init__.py index 6e62bfb9..fbb2f424 100644 --- a/telethon/errors/__init__.py +++ b/telethon/errors/__init__.py @@ -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, diff --git a/telethon/errors/common.py b/telethon/errors/common.py index be3b1d93..f2f21840 100644 --- a/telethon/errors/common.py +++ b/telethon/errors/common.py @@ -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.' diff --git a/telethon/errors/rpc_base_errors.py b/telethon/errors/rpc_base_errors.py index 5c938641..9e6eed1a 100644 --- a/telethon/errors/rpc_base_errors.py +++ b/telethon/errors/rpc_base_errors.py @@ -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): diff --git a/telethon/extensions/binary_reader.py b/telethon/extensions/binary_reader.py index 2355c6a4..19fb608b 100644 --- a/telethon/extensions/binary_reader.py +++ b/telethon/extensions/binary_reader.py @@ -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(' '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] diff --git a/telethon/extensions/tcp_client.py b/telethon/extensions/tcp_client.py index 5255513a..f59bb9f0 100644 --- a/telethon/extensions/tcp_client.py +++ b/telethon/extensions/tcp_client.py @@ -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.') diff --git a/telethon/network/__init__.py b/telethon/network/__init__.py index 77bd4406..d2538924 100644 --- a/telethon/network/__init__.py +++ b/telethon/network/__init__.py @@ -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 diff --git a/telethon/network/authenticator.py b/telethon/network/authenticator.py index 1accf493..a73bae38 100644 --- a/telethon/network/authenticator.py +++ b/telethon/network/authenticator.py @@ -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) diff --git a/telethon/network/connection.py b/telethon/network/connection.py index fe04352f..ff255d00 100644 --- a/telethon/network/connection.py +++ b/telethon/network/connection.py @@ -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('= 127: length = struct.unpack('> 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 diff --git a/telethon/network/mtproto_plain_sender.py b/telethon/network/mtproto_plain_sender.py index c7c021be..cb6d63af 100644 --- a/telethon/network/mtproto_plain_sender.py +++ b/telethon/network/mtproto_plain_sender.py @@ -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(' 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 diff --git a/telethon/telegram_client.py b/telethon/telegram_client.py index 4a6e25e4..32ade1a9 100644 --- a/telethon/telegram_client.py +++ b/telethon/telegram_client.py @@ -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 diff --git a/telethon/tl/custom/__init__.py b/telethon/tl/custom/__init__.py index 40914f16..5b6bf44d 100644 --- a/telethon/tl/custom/__init__.py +++ b/telethon/tl/custom/__init__.py @@ -1 +1,2 @@ from .draft import Draft +from .dialog import Dialog diff --git a/telethon/tl/custom/dialog.py b/telethon/tl/custom/dialog.py new file mode 100644 index 00000000..bac8b0de --- /dev/null +++ b/telethon/tl/custom/dialog.py @@ -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) diff --git a/telethon/tl/entity_database.py b/telethon/tl/entity_database.py index b0fc70fb..9002ebd8 100644 --- a/telethon/tl/entity_database.py +++ b/telethon/tl/entity_database.py @@ -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) diff --git a/telethon/tl/message_container.py b/telethon/tl/message_container.py index 12d617cc..58fb8021 100644 --- a/telethon/tl/message_container.py +++ b/telethon/tl/message_container.py @@ -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( ' 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) diff --git a/telethon/tl/tl_message.py b/telethon/tl/tl_message.py index b524b75b..bcb48279 100644 --- a/telethon/tl/tl_message.py +++ b/telethon/tl/tl_message.py @@ -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(' log works diff --git a/telethon/version.py b/telethon/version.py new file mode 100644 index 00000000..096fbd6c --- /dev/null +++ b/telethon/version.py @@ -0,0 +1,3 @@ +# Versions should comply with PEP440. +# This line is parsed in setup.py: +__version__ = '0.15.5' diff --git a/telethon_examples/anytime.png b/telethon_examples/anytime.png new file mode 100644 index 00000000..c8663cfa Binary files /dev/null and b/telethon_examples/anytime.png differ diff --git a/telethon_examples/interactive_telegram_client.py b/telethon_examples/interactive_telegram_client.py index ee179a42..52c2c356 100644 --- a/telethon_examples/interactive_telegram_client.py +++ b/telethon_examples/interactive_telegram_client.py @@ -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 )) diff --git a/telethon_examples/replier.py b/telethon_examples/replier.py index 954b9878..66026363 100755 --- a/telethon_examples/replier.py +++ b/telethon_examples/replier.py @@ -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() diff --git a/telethon_generator/error_descriptions b/telethon_generator/error_descriptions index 500504d7..65894ba1 100644 --- a/telethon_generator/error_descriptions +++ b/telethon_generator/error_descriptions @@ -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 diff --git a/telethon_generator/parser/tl_parser.py b/telethon_generator/parser/tl_parser.py index a08521db..8c24cbf4 100644 --- a/telethon_generator/parser/tl_parser.py +++ b/telethon_generator/parser/tl_parser.py @@ -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) diff --git a/telethon_generator/scheme.tl b/telethon_generator/scheme.tl index 5e949239..2ecb31b4 100644 --- a/telethon_generator/scheme.tl +++ b/telethon_generator/scheme.tl @@ -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 caption:string stickers:flags.0?Vector 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 caption:string stickers:flags.0?Vector 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 = 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 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 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 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 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 = 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 = 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 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 = Config; +config#9c840964 flags:# phonecalls_enabled:flags.1?true date:int expires:int test_mode:Bool this_dc:int dc_options:Vector 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 = 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 users:Vector = channels.ChannelParticipants; +channels.channelParticipantsNotModified#f0173fe9 = channels.ChannelParticipants; channels.channelParticipant#d0d9b163 participant:ChannelParticipant users:Vector = channels.ChannelParticipant; @@ -680,7 +689,7 @@ messages.savedGifs#2e0709a5 hash:int gifs:Vector = 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 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 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 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 cache_time:int users:Vector = 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 stickers:Vector = messages.FavedStickers; +help.recentMeUrls#e0310d7 urls:Vector chats:Vector users:Vector = 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; 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 = 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 random_id:Vector 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 random_id:Vector 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 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 = 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 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 = 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 = 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 = messages.AffectedMessages; channels.deleteUserHistory#d10dd71b channel:InputChannel user_id:InputUser = messages.AffectedHistory; channels.reportSpam#fe087810 channel:InputChannel user_id:InputUser id:Vector = Bool; channels.getMessages#93d7b347 channel:InputChannel id:Vector = 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 = 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 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 = 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 = Vector; -// LAYER 71 +// LAYER 73 diff --git a/telethon_generator/tl_generator.py b/telethon_generator/tl_generator.py index b5d43656..f8a9e873 100644 --- a/telethon_generator/tl_generator.py +++ b/telethon_generator/tl_generator.py @@ -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('