pax_global_header00006660000000000000000000000064147556626500014532gustar00rootroot0000000000000052 comment=045df418dfbfe7858288a0a1f8e287260e5e852d Telethon-1.39.0/000077500000000000000000000000001475566265000134065ustar00rootroot00000000000000Telethon-1.39.0/.coveragerc000066400000000000000000000001221475566265000155220ustar00rootroot00000000000000[run] branch = true parallel = true source = telethon [report] precision = 2 Telethon-1.39.0/.github/000077500000000000000000000000001475566265000147465ustar00rootroot00000000000000Telethon-1.39.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001475566265000171315ustar00rootroot00000000000000Telethon-1.39.0/.github/ISSUE_TEMPLATE/bug-report.yml000066400000000000000000000052261475566265000217470ustar00rootroot00000000000000name: Bug Report description: Create a report about a bug inside the library. body: - type: textarea id: reproducing-example attributes: label: Code that causes the issue description: Provide a code example that reproduces the problem. Try to keep it short without other dependencies. placeholder: | ```python from telethon.sync import TelegramClient ... ``` validations: required: true - type: textarea id: expected-behavior attributes: label: Expected behavior description: Explain what you should expect to happen. Include reproduction steps. placeholder: | "I was doing... I was expecting the following to happen..." validations: required: true - type: textarea id: actual-behavior attributes: label: Actual behavior description: Explain what actually happens. placeholder: | "This happened instead..." validations: required: true - type: textarea id: traceback attributes: label: Traceback description: | The traceback, if the problem is a crash. placeholder: | ``` Traceback (most recent call last): File "code.py", line 1, in ``` - type: input id: telethon-version attributes: label: Telethon version description: The output of `python -c "import telethon; print(telethon.__version__)"`. placeholder: "1.x" validations: required: true - type: input id: python-version attributes: label: Python version description: The output of `python --version`. placeholder: "3.x" validations: required: true - type: input id: os attributes: label: Operating system (including distribution name and version) placeholder: Windows 11, macOS 13.4, Ubuntu 23.04... validations: required: true - type: textarea id: other-details attributes: label: Other details placeholder: | Additional details and attachments. Is it a server? Network condition? - type: checkboxes id: checklist attributes: label: Checklist description: Read this carefully, we will close and ignore your issue if you skimmed through this. options: - label: The error is in the library's code, and not in my own. required: true - label: I have searched for this issue before posting it and there isn't an open duplicate. required: true - label: I ran `pip install -U https://github.com/LonamiWebs/Telethon/archive/v1.zip` and triggered the bug in the latest version. required: true Telethon-1.39.0/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000007261475566265000211260ustar00rootroot00000000000000blank_issues_enabled: false contact_links: - name: Ask questions in StackOverflow url: https://stackoverflow.com/questions/ask?tags=telethon about: Questions are not bugs. Please ask them in StackOverflow instead. Questions in the bug tracker will be closed - name: Find about updates and our Telegram groups url: https://t.me/s/TelethonUpdates about: Be notified of updates, chat with other people about the library or ask questions in these groups Telethon-1.39.0/.github/ISSUE_TEMPLATE/documentation-issue.yml000066400000000000000000000013071475566265000236540ustar00rootroot00000000000000name: Documentation Issue description: Report a problem with the documentation. labels: [documentation] body: - type: textarea id: description attributes: label: Description description: Describe the problem in detail. placeholder: This part is unclear... - type: checkboxes id: checklist attributes: label: Checklist description: Read this carefully, we will close and ignore your issue if you skimmed through this. options: - label: This is a documentation problem, not a question or a bug report. required: true - label: I have searched for this issue before posting it and there isn't a duplicate. required: true Telethon-1.39.0/.github/ISSUE_TEMPLATE/feature-request.yml000066400000000000000000000014451475566265000230010ustar00rootroot00000000000000name: Feature Request description: Suggest ideas, changes or other enhancements for the library. labels: [enhancement] body: - type: textarea id: feature-description attributes: label: Describe your suggested feature description: Please describe your idea. Would you like another friendly method? Renaming them to something more appropriate? Changing the way something works? placeholder: "It should work like this..." validations: required: true - type: checkboxes id: checklist attributes: label: Checklist description: Read this carefully, we will close and ignore your issue if you skimmed through this. options: - label: I have searched for this issue before posting it and there isn't a duplicate. required: true Telethon-1.39.0/.github/pull_request_template.md000066400000000000000000000003041475566265000217040ustar00rootroot00000000000000 Telethon-1.39.0/.github/workflows.disabled/000077500000000000000000000000001475566265000205515ustar00rootroot00000000000000Telethon-1.39.0/.github/workflows.disabled/python.yml000066400000000000000000000012161475566265000226150ustar00rootroot00000000000000name: Python Library on: [push, pull_request] jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.5", "3.6", "3.7", "3.8"] steps: - uses: actions/checkout@v1 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} - name: Set up env run: | python -m pip install --upgrade pip pip install tox - name: Lint with flake8 run: | tox -e flake - name: Test with pytest run: | # use "py", which is the default python version tox -e py Telethon-1.39.0/.gitignore000066400000000000000000000005501475566265000153760ustar00rootroot00000000000000# Generated code /telethon/tl/functions/ /telethon/tl/types/ /telethon/tl/alltlobjects.py /telethon/errors/rpcerrorlist.py # User session *.session /usermedia/ # Builds and testing __pycache__/ /dist/ /build/ /*.egg-info/ /readthedocs/_build/ /.tox/ # API reference docs /docs/ # File used to manually test new changes, contains sensitive data /example.py Telethon-1.39.0/.pre-commit-config.yaml000066400000000000000000000012221475566265000176640ustar00rootroot00000000000000- repo: git://github.com/pre-commit/pre-commit-hooks sha: 7539d8bd1a00a3c1bfd34cdb606d3a6372e83469 hooks: - id: check-added-large-files - id: check-case-conflict - id: check-merge-conflict - id: check-symlinks - id: check-yaml - id: double-quote-string-fixer - id: end-of-file-fixer - id: name-tests-test - id: trailing-whitespace - repo: git://github.com/pre-commit/mirrors-yapf sha: v0.11.1 hooks: - id: yapf - repo: git://github.com/FalconSocial/pre-commit-python-sorter sha: 1.0.4 hooks: - id: python-import-sorter args: - --silent-overwrite Telethon-1.39.0/.readthedocs.yaml000066400000000000000000000004141475566265000166340ustar00rootroot00000000000000# https://docs.readthedocs.io/en/stable/config-file/v2.html version: 2 build: os: ubuntu-22.04 tools: python: "3.11" sphinx: configuration: readthedocs/conf.py formats: - pdf - epub python: install: - requirements: readthedocs/requirements.txt Telethon-1.39.0/LICENSE000077500000000000000000000020631475566265000144170ustar00rootroot00000000000000MIT License Copyright (c) 2016-Present LonamiWebs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Telethon-1.39.0/README.rst000077500000000000000000000047771475566265000151170ustar00rootroot00000000000000Telethon ======== .. epigraph:: ⭐️ Thanks **everyone** who has starred the project, it means a lot! |logo| **Telethon** is an asyncio_ **Python 3** MTProto_ library to interact with Telegram_'s API as a user or through a bot account (bot API alternative). .. important:: If you have code using Telethon before its 1.0 version, you must read `Compatibility and Convenience`_ to learn how to migrate. As with any third-party library for Telegram, be careful not to break `Telegram's ToS`_ or `Telegram can ban the account`_. What is this? ------------- Telegram is a popular messaging application. This library is meant to make it easy for you to write Python programs that can interact with Telegram. Think of it as a wrapper that has already done the heavy job for you, so you can focus on developing an application. Installing ---------- .. code-block:: sh pip3 install telethon Creating a client ----------------- .. code-block:: python from telethon import TelegramClient, events, sync # 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' client = TelegramClient('session_name', api_id, api_hash) client.start() Doing stuff ----------- .. code-block:: python print(client.get_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') messages = client.get_messages('username') messages[0].download_media() @client.on(events.NewMessage(pattern='(?i)hi|hello')) async def handler(event): await event.respond('Hey!') Next steps ---------- Do you like how Telethon looks? Check out `Read The Docs`_ for a more in-depth explanation, with examples, troubleshooting issues, and more useful information. .. _asyncio: https://docs.python.org/3/library/asyncio.html .. _MTProto: https://core.telegram.org/mtproto .. _Telegram: https://telegram.org .. _Compatibility and Convenience: https://docs.telethon.dev/en/stable/misc/compatibility-and-convenience.html .. _Telegram's ToS: https://core.telegram.org/api/terms .. _Telegram can ban the account: https://docs.telethon.dev/en/stable/quick-references/faq.html#my-account-was-deleted-limited-when-using-the-library .. _Read The Docs: https://docs.telethon.dev .. |logo| image:: logo.svg :width: 24pt :height: 24pt Telethon-1.39.0/dev-requirements.txt000066400000000000000000000000411475566265000174410ustar00rootroot00000000000000pytest pytest-cov pytest-asyncio Telethon-1.39.0/logo.svg000066400000000000000000000006251475566265000150720ustar00rootroot00000000000000 Telethon-1.39.0/optional-requirements.txt000066400000000000000000000000641475566265000205150ustar00rootroot00000000000000cryptg pysocks python-socks[asyncio] hachoir pillow Telethon-1.39.0/pyproject.toml000066400000000000000000000022161475566265000163230ustar00rootroot00000000000000# https://snarky.ca/what-the-heck-is-pyproject-toml/ [build-system] requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta" # Need to use legacy format for the time being # https://tox.readthedocs.io/en/3.20.0/example/basic.html#pyproject-toml-tox-legacy-ini [tool.tox] legacy_tox_ini = """ [tox] envlist = py35,py36,py37,py38 # run with tox -e py [testenv] deps = -rrequirements.txt -roptional-requirements.txt -rdev-requirements.txt commands = # NOTE: you can run any command line tool here - not just tests pytest {posargs} # run with tox -e flake [testenv:flake] deps = -rrequirements.txt -roptional-requirements.txt -rdev-requirements.txt flake8 commands = # stop the build if there are Python syntax errors or undefined names flake8 telethon/ telethon_generator/ tests/ --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 telethon/ telethon_generator/ tests/ --count --exit-zero --exclude telethon/tl/,telethon/errors/rpcerrorlist.py --max-complexity=10 --max-line-length=127 --statistics """ Telethon-1.39.0/readthedocs/000077500000000000000000000000001475566265000156735ustar00rootroot00000000000000Telethon-1.39.0/readthedocs/Makefile000066400000000000000000000011351475566265000173330ustar00rootroot00000000000000# 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)Telethon-1.39.0/readthedocs/basic/000077500000000000000000000000001475566265000167545ustar00rootroot00000000000000Telethon-1.39.0/readthedocs/basic/installation.rst000066400000000000000000000061111475566265000222060ustar00rootroot00000000000000.. _installation: ============ Installation ============ Telethon is a Python library, which means you need to download and install Python from https://www.python.org/downloads/ if you haven't already. Once you have Python installed, `upgrade pip`__ and run: .. code-block:: sh python3 -m pip install --upgrade pip python3 -m pip install --upgrade telethon …to install or upgrade the library to the latest version. .. __: https://pythonspeed.com/articles/upgrade-pip/ Installing Development Versions =============================== If you want the *latest* unreleased changes, you can run the following command instead: .. code-block:: sh python3 -m pip install --upgrade https://github.com/LonamiWebs/Telethon/archive/v1.zip .. note:: The development version may have bugs and is not recommended for production use. However, when you are `reporting a library bug`__, you should try if the bug still occurs in this version. .. __: https://github.com/LonamiWebs/Telethon/issues/ Verification ============ To verify that the library is installed correctly, run the following command: .. code-block:: sh python3 -c "import telethon; print(telethon.__version__)" The version number of the library should show in the output. Optional Dependencies ===================== If cryptg_ is installed, **the library will work a lot faster**, since encryption and decryption will be made in C instead of Python. If your code deals with a lot of updates or you are downloading/uploading a lot of files, you will notice a considerable speed-up (from a hundred kilobytes per second to several megabytes per second, if your connection allows it). If it's not installed, pyaes_ will be used (which is pure Python, so it's much slower). If pillow_ is installed, large images will be automatically resized when sending photos to prevent Telegram from failing with "invalid image". Official clients also do this. If aiohttp_ is installed, the library will be able to download :tl:`WebDocument` media files (otherwise you will get an error). If hachoir_ is installed, it will be used to extract metadata from files when sending documents. Telegram uses this information to show the song's performer, artist, title, duration, and for videos too (including size). Otherwise, they will default to empty values, and you can set the attributes manually. .. note:: Some of the modules may require additional dependencies before being installed through ``pip``. If you have an ``apt``-based system, consider installing the most commonly missing dependencies (with the right ``pip``): .. code-block:: sh apt update apt install clang lib{jpeg-turbo,webp}-dev python{,-dev} zlib-dev pip install -U --user setuptools pip install -U --user telethon cryptg pillow Thanks to `@bb010g`_ for writing down this nice list. .. _cryptg: https://github.com/cher-nov/cryptg .. _pyaes: https://github.com/ricmoo/pyaes .. _pillow: https://python-pillow.org .. _aiohttp: https://docs.aiohttp.org .. _hachoir: https://hachoir.readthedocs.io .. _@bb010g: https://static.bb010g.com Telethon-1.39.0/readthedocs/basic/next-steps.rst000066400000000000000000000041161475566265000216220ustar00rootroot00000000000000========== Next Steps ========== These basic first steps should have gotten you started with the library. By now, you should know how to call friendly methods and how to work with the returned objects, how things work inside event handlers, etc. Next, we will see a quick reference summary of *all* the methods and properties that you will need when using the library. If you follow the links there, you will expand the documentation for the method and property, with more examples on how to use them. Therefore, **you can find an example on every method** of the client to learn how to use it, as well as a description of all the arguments. After that, we will go in-depth with some other important concepts that are worth learning and understanding. From now on, you can keep pressing the "Next" button if you want, or use the menu on the left, since some pages are quite lengthy. A note on developing applications ================================= If you're using the library to make an actual application (and not just automate things), you should make sure to `comply with the ToS`__: […] when logging in as an existing user, apps are supposed to call [:tl:`GetTermsOfServiceUpdate`] to check for any updates to the Terms of Service; this call should be repeated after ``expires`` seconds have elapsed. If an update to the Terms Of Service is available, clients are supposed to show a consent popup; if accepted, clients should call [:tl:`AcceptTermsOfService`], providing the ``termsOfService id`` JSON object; in case of denial, clients are to delete the account using [:tl:`DeleteAccount`], providing Decline ToS update as deletion reason. .. __: https://core.telegram.org/api/config#terms-of-service However, if you use the library to automate or enhance your Telegram experience, it's very likely that you are using other applications doing this check for you (so you wouldn't run the risk of violating the ToS). The library itself will not automatically perform this check or accept the ToS because it should require user action (the only exception is during sign-up). Telethon-1.39.0/readthedocs/basic/quick-start.rst000066400000000000000000000077101475566265000217620ustar00rootroot00000000000000=========== Quick-Start =========== Let's see a longer example to learn some of the methods that the library has to offer. These are known as "friendly methods", and you should always use these if possible. .. code-block:: python from telethon import TelegramClient # Remember to use your own values from my.telegram.org! api_id = 12345 api_hash = '0123456789abcdef0123456789abcdef' client = TelegramClient('anon', api_id, api_hash) async def main(): # Getting information about yourself me = await client.get_me() # "me" is a user object. You can pretty-print # any Telegram object with the "stringify" method: print(me.stringify()) # When you print something, you see a representation of it. # You can access all attributes of Telegram objects with # the dot operator. For example, to get the username: username = me.username print(username) print(me.phone) # You can print all the dialogs/conversations that you are part of: async for dialog in client.iter_dialogs(): print(dialog.name, 'has ID', dialog.id) # You can send messages to yourself... await client.send_message('me', 'Hello, myself!') # ...to some chat ID await client.send_message(-100123456, 'Hello, group!') # ...to your contacts await client.send_message('+34600123123', 'Hello, friend!') # ...or even to any username await client.send_message('username', 'Testing Telethon!') # You can, of course, use markdown in your messages: message = await client.send_message( 'me', 'This message has **bold**, `code`, __italics__ and ' 'a [nice website](https://example.com)!', link_preview=False ) # Sending a message returns the sent message object, which you can use print(message.raw_text) # You can reply to messages directly if you have a message object await message.reply('Cool!') # Or send files, songs, documents, albums... await client.send_file('me', '/home/me/Pictures/holidays.jpg') # You can print the message history of any chat: async for message in client.iter_messages('me'): print(message.id, message.text) # You can download media from messages, too! # The method will return the path where the file was saved. if message.photo: path = await message.download_media() print('File saved to', path) # printed after download is done with client: client.loop.run_until_complete(main()) Here, we show how to sign in, get information about yourself, send messages, files, getting chats, printing messages, and downloading files. You should make sure that you understand what the code shown here does, take note on how methods are called and used and so on before proceeding. We will see all the available methods later on. .. important:: Note that Telethon is an asynchronous library, and as such, you should get used to it and learn a bit of basic `asyncio`. This will help a lot. As a quick start, this means you generally want to write all your code inside some ``async def`` like so: .. code-block:: python client = ... async def do_something(me): ... async def main(): # Most of your code should go here. # You can of course make and use your own async def (do_something). # They only need to be async if they need to await things. me = await client.get_me() await do_something(me) with client: client.loop.run_until_complete(main()) After you understand this, you may use the ``telethon.sync`` hack if you want do so (see :ref:`compatibility-and-convenience`), but note you may run into other issues (iPython, Anaconda, etc. have some issues with it). Telethon-1.39.0/readthedocs/basic/signing-in.rst000066400000000000000000000155551475566265000215630ustar00rootroot00000000000000.. _signing-in: ========== Signing In ========== Before working with Telegram's API, you need to get your own API ID and hash: 1. `Login to your Telegram account `_ with the phone number of the developer account to use. 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 currently be changed later. 4. Click on *Create application* at the end. Remember that your **API hash is secret** and Telegram won't let you revoke it. Don't post it anywhere! .. note:: This API ID and hash is the one used by *your application*, not your phone number. You can use this API ID and hash with *any* phone number or even for bot accounts. Editing the Code ================ This is a little introduction for those new to Python programming in general. We will write our code inside ``hello.py``, so you can use any text editor that you like. To run the code, use ``python3 hello.py`` from the terminal. .. important:: Don't call your script ``telethon.py``! Python will try to import the client from there and it will fail with an error such as "ImportError: cannot import name 'TelegramClient' ...". Signing In ========== We can finally write some code to log into our account! .. code-block:: python from telethon import TelegramClient # Use your own values from my.telegram.org api_id = 12345 api_hash = '0123456789abcdef0123456789abcdef' # The first parameter is the .session file name (absolute paths allowed) with TelegramClient('anon', api_id, api_hash) as client: client.loop.run_until_complete(client.send_message('me', 'Hello, myself!')) In the first line, we import the class name so we can create an instance of the client. Then, we define variables to store our API ID and hash conveniently. At last, we create a new `TelegramClient ` instance and call it ``client``. We can now use the client variable for anything that we want, such as sending a message to ourselves. .. note:: Since Telethon is an asynchronous library, you need to ``await`` coroutine functions to have them run (or otherwise, run the loop until they are complete). In this tiny example, we don't bother making an ``async def main()``. See :ref:`mastering-asyncio` to find out more. Using a ``with`` block is the preferred way to use the library. It will automatically `start() ` the client, logging or signing up if necessary. If the ``.session`` file already existed, it will not login again, so be aware of this if you move or rename the file! Signing In as a Bot Account =========================== You can also use Telethon for your bots (normal bot accounts, not users). You will still need an API ID and hash, but the process is very similar: .. code-block:: python from telethon.sync import TelegramClient api_id = 12345 api_hash = '0123456789abcdef0123456789abcdef' bot_token = '12345:0123456789abcdef0123456789abcdef' # We have to manually call "start" if we want an explicit bot token bot = TelegramClient('bot', api_id, api_hash).start(bot_token=bot_token) # But then we can use the client instance as usual with bot: ... To get a bot account, you need to talk with `@BotFather `_. Signing In behind a Proxy ========================= If you need to use a proxy to access Telegram, you will need to either: * For Python >= 3.6 : `install python-socks[asyncio]`__ * For Python <= 3.5 : `install PySocks`__ and then change .. code-block:: python TelegramClient('anon', api_id, api_hash) with .. code-block:: python TelegramClient('anon', api_id, api_hash, proxy=("socks5", '127.0.0.1', 4444)) (of course, replacing the protocol, IP and port with the protocol, IP and port of the proxy). The ``proxy=`` argument should be a dict (or tuple, for backwards compatibility), consisting of parameters described `in PySocks usage`__. The allowed values for the argument ``proxy_type`` are: * For Python <= 3.5: * ``socks.SOCKS5`` or ``'socks5'`` * ``socks.SOCKS4`` or ``'socks4'`` * ``socks.HTTP`` or ``'http'`` * For Python >= 3.6: * All of the above * ``python_socks.ProxyType.SOCKS5`` * ``python_socks.ProxyType.SOCKS4`` * ``python_socks.ProxyType.HTTP`` Example: .. code-block:: python proxy = { 'proxy_type': 'socks5', # (mandatory) protocol to use (see above) 'addr': '1.1.1.1', # (mandatory) proxy IP address 'port': 5555, # (mandatory) proxy port number 'username': 'foo', # (optional) username if the proxy requires auth 'password': 'bar', # (optional) password if the proxy requires auth 'rdns': True # (optional) whether to use remote or local resolve, default remote } For backwards compatibility with ``PySocks`` the following format is possible (but discouraged): .. code-block:: python proxy = (socks.SOCKS5, '1.1.1.1', 5555, True, 'foo', 'bar') .. __: https://github.com/romis2012/python-socks#installation .. __: https://github.com/Anorov/PySocks#installation .. __: https://github.com/Anorov/PySocks#usage-1 Using MTProto Proxies ===================== MTProto Proxies are Telegram's alternative to normal proxies, and work a bit differently. The following protocols are available: * ``ConnectionTcpMTProxyAbridged`` * ``ConnectionTcpMTProxyIntermediate`` * ``ConnectionTcpMTProxyRandomizedIntermediate`` (preferred) For now, you need to manually specify these special connection modes if you want to use a MTProto Proxy. Your code would look like this: .. code-block:: python from telethon import TelegramClient, connection # we need to change the connection ^^^^^^^^^^ client = TelegramClient( 'anon', api_id, api_hash, # Use one of the available connection modes. # Normally, this one works with most proxies. connection=connection.ConnectionTcpMTProxyRandomizedIntermediate, # Then, pass the proxy details as a tuple: # (host name, port, proxy secret) # # If the proxy has no secret, the secret must be: # '00000000000000000000000000000000' proxy=('mtproxy.example.com', 2002, 'secret') ) In future updates, we may make it easier to use MTProto Proxies (such as avoiding the need to manually pass ``connection=``). In short, the same code above but without comments to make it clearer: .. code-block:: python from telethon import TelegramClient, connection client = TelegramClient( 'anon', api_id, api_hash, connection=connection.ConnectionTcpMTProxyRandomizedIntermediate, proxy=('mtproxy.example.com', 2002, 'secret') ) Telethon-1.39.0/readthedocs/basic/updates.rst000066400000000000000000000112371475566265000211570ustar00rootroot00000000000000======= Updates ======= Updates are an important topic in a messaging platform like Telegram. After all, you want to be notified when a new message arrives, when a member joins, when someone starts typing, etc. For that, you can use **events**. .. important:: It is strongly advised to enable logging when working with events, since exceptions in event handlers are hidden by default. Please add the following snippet to the very top of your file: .. code-block:: python import logging logging.basicConfig(format='[%(levelname) %(asctime)s] %(name)s: %(message)s', level=logging.WARNING) Getting Started =============== Let's start things with an example to automate replies: .. code-block:: python from telethon import TelegramClient, events client = TelegramClient('anon', api_id, api_hash) @client.on(events.NewMessage) async def my_event_handler(event): if 'hello' in event.raw_text: await event.reply('hi!') client.start() client.run_until_disconnected() This code isn't much, but there might be some things unclear. Let's break it down: .. code-block:: python from telethon import TelegramClient, events client = TelegramClient('anon', api_id, api_hash) This is normal creation (of course, pass session name, API ID and hash). Nothing we don't know already. .. code-block:: python @client.on(events.NewMessage) This Python decorator will attach itself to the ``my_event_handler`` definition, and basically means that *on* a `NewMessage ` *event*, the callback function you're about to define will be called: .. code-block:: python async def my_event_handler(event): if 'hello' in event.raw_text: await event.reply('hi!') If a `NewMessage ` event occurs, and ``'hello'`` is in the text of the message, we `reply() ` to the event with a ``'hi!'`` message. .. note:: Event handlers **must** be ``async def``. After all, Telethon is an asynchronous library based on `asyncio`, which is a safer and often faster approach to threads. You **must** ``await`` all method calls that use network requests, which is most of them. More Examples ============= Replying to messages with hello is fun, but, can we do more? .. code-block:: python @client.on(events.NewMessage(outgoing=True, pattern=r'\.save')) async def handler(event): if event.is_reply: replied = await event.get_reply_message() sender = replied.sender await client.download_profile_photo(sender) await event.respond('Saved your photo {}'.format(sender.username)) We could also get replies. This event filters outgoing messages (only those that we send will trigger the method), then we filter by the regex ``r'\.save'``, which will match messages starting with ``".save"``. Inside the method, we check whether the event is replying to another message or not. If it is, we get the reply message and the sender of that message, and download their profile photo. Let's delete messages which contain "heck". We don't allow swearing here. .. code-block:: python @client.on(events.NewMessage(pattern=r'(?i).*heck')) async def handler(event): await event.delete() With the ``r'(?i).*heck'`` regex, we match case-insensitive "heck" anywhere in the message. Regex is very powerful and you can learn more at https://regexone.com/. So far, we have only seen the `NewMessage `, but there are many more which will be covered later. This is only a small introduction to updates. Entities ======== When you need the user or chat where an event occurred, you **must** use the following methods: .. code-block:: python async def handler(event): # Good chat = await event.get_chat() sender = await event.get_sender() chat_id = event.chat_id sender_id = event.sender_id # BAD. Don't do this chat = event.chat sender = event.sender chat_id = event.chat.id sender_id = event.sender.id Events are like messages, but don't have all the information a message has! When you manually get a message, it will have all the information it needs. When you receive an update about a message, it **won't** have all the information, so you have to **use the methods**, not the properties. Make sure you understand the code seen here before continuing! As a rule of thumb, remember that new message events behave just like message objects, so you can do with them everything you can do with a message object. Telethon-1.39.0/readthedocs/concepts/000077500000000000000000000000001475566265000175115ustar00rootroot00000000000000Telethon-1.39.0/readthedocs/concepts/asyncio.rst000066400000000000000000000267361475566265000217260ustar00rootroot00000000000000.. _mastering-asyncio: ================= Mastering asyncio ================= .. contents:: What's asyncio? =============== `asyncio` is a Python 3's built-in library. This means it's already installed if you have Python 3. Since Python 3.5, it is convenient to work with asynchronous code. Before (Python 3.4) we didn't have ``async`` or ``await``, but now we do. `asyncio` stands for *Asynchronous Input Output*. This is a very powerful concept to use whenever you work IO. Interacting with the web or external APIs such as Telegram's makes a lot of sense this way. Why asyncio? ============ Asynchronous IO makes a lot of sense in a library like Telethon. You send a request to the server (such as "get some message"), and thanks to `asyncio`, your code won't block while a response arrives. The alternative would be to spawn a thread for each update so that other code can run while the response arrives. That is *a lot* more expensive. The code will also run faster, because instead of switching back and forth between the OS and your script, your script can handle it all. Avoiding switching saves quite a bit of time, in Python or any other language that supports asynchronous IO. It will also be cheaper, because tasks are smaller than threads, which are smaller than processes. What are asyncio basics? ======================== The code samples below assume that you have Python 3.7 or greater installed. .. code-block:: python # First we need the asyncio library import asyncio # We also need something to run async def main(): for char in 'Hello, world!\n': print(char, end='', flush=True) await asyncio.sleep(0.2) # Then, we can create a new asyncio loop and use it to run our coroutine. # The creation and tear-down of the loop is hidden away from us. asyncio.run(main()) What does telethon.sync do? =========================== The moment you import any of these: .. code-block:: python from telethon import sync, ... # or from telethon.sync import ... # or import telethon.sync The ``sync`` module rewrites most ``async def`` methods in Telethon to something similar to this: .. code-block:: python def new_method(): result = original_method() if loop.is_running(): # the loop is already running, return the await-able to the user return result else: # the loop is not running yet, so we can run it for the user return loop.run_until_complete(result) That means you can do this: .. code-block:: python print(client.get_me().username) Instead of this: .. code-block:: python me = client.loop.run_until_complete(client.get_me()) print(me.username) # or, using asyncio's default loop (it's the same) import asyncio loop = asyncio.get_running_loop() # == client.loop me = loop.run_until_complete(client.get_me()) print(me.username) As you can see, it's a lot of boilerplate and noise having to type ``run_until_complete`` all the time, so you can let the magic module to rewrite it for you. But notice the comment above: it won't run the loop if it's already running, because it can't. That means this: .. code-block:: python async def main(): # 3. the loop is running here print( client.get_me() # 4. this will return a coroutine! .username # 5. this fails, coroutines don't have usernames ) loop.run_until_complete( # 2. run the loop and the ``main()`` coroutine main() # 1. calling ``async def`` "returns" a coroutine ) Will fail. So if you're inside an ``async def``, then the loop is running, and if the loop is running, you must ``await`` things yourself: .. code-block:: python async def main(): print((await client.get_me()).username) loop.run_until_complete(main()) What are async, await and coroutines? ===================================== The ``async`` keyword lets you define asynchronous functions, also known as coroutines, and also iterate over asynchronous loops or use ``async with``: .. code-block:: python import asyncio async def main(): # ^ this declares the main() coroutine function async with client: # ^ this is an asynchronous with block async for message in client.iter_messages(chat): # ^ this is a for loop over an asynchronous generator print(message.sender.username) asyncio.run(main()) # ^ this will create a new asyncio loop behind the scenes and tear it down # once the function returns. It will run the loop untiil main finishes. # You should only use this function if there is no other loop running. The ``await`` keyword blocks the *current* task, and the loop can run other tasks. Tasks can be thought of as "threads", since many can run concurrently: .. code-block:: python import asyncio async def hello(delay): await asyncio.sleep(delay) # await tells the loop this task is "busy" print('hello') # eventually the loop resumes the code here async def world(delay): # the loop decides this method should run first await asyncio.sleep(delay) # await tells the loop this task is "busy" print('world') # eventually the loop finishes all tasks async def main(): asyncio.create_task(world(2)) # create the world task, passing 2 as delay asyncio.create_task(hello(delay=1)) # another task, but with delay 1 await asyncio.sleep(3) # wait for three seconds before exiting try: # create a new temporary asyncio loop and use it to run main asyncio.run(main()) except KeyboardInterrupt: pass The same example, but without the comment noise: .. code-block:: python import asyncio async def hello(delay): await asyncio.sleep(delay) print('hello') async def world(delay): await asyncio.sleep(delay) print('world') async def main(): asyncio.create_task(world(2)) asyncio.create_task(hello(delay=1)) await asyncio.sleep(3) try: asyncio.run(main()) except KeyboardInterrupt: pass Can I use threads? ================== Yes, you can, but you must understand that the loops themselves are not thread safe. and you must be sure to know what is happening. The easiest and cleanest option is to use `asyncio.run` to create and manage the new event loop for you: .. code-block:: python import asyncio import threading async def actual_work(): client = TelegramClient(..., loop=loop) ... # can use `await` here def go(): asyncio.run(actual_work()) threading.Thread(target=go).start() Generally, **you don't need threads** unless you know what you're doing. Just create another task, as shown above. If you're using the Telethon with a library that uses threads, you must be careful to use `threading.Lock` whenever you use the client, or enable the compatible mode. For that, see :ref:`compatibility-and-convenience`. You may have seen this error: .. code-block:: text RuntimeError: There is no current event loop in thread 'Thread-1'. It just means you didn't create a loop for that thread. Please refer to the ``asyncio`` documentation to correctly learn how to set the event loop for non-main threads. client.run_until_disconnected() blocks! ======================================= All of what `client.run_until_disconnected() ` does is run the `asyncio`'s event loop until the client is disconnected. That means *the loop is running*. And if the loop is running, it will run all the tasks in it. So if you want to run *other* code, create tasks for it: .. code-block:: python from datetime import datetime async def clock(): while True: print('The time:', datetime.now()) await asyncio.sleep(1) loop.create_task(clock()) ... client.run_until_disconnected() This creates a task for a clock that prints the time every second. You don't need to use `client.run_until_disconnected() ` either! You just need to make the loop is running, somehow. `loop.run_forever() ` and `loop.run_until_complete() ` can also be used to run the loop, and Telethon will be happy with any approach. Of course, there are better tools to run code hourly or daily, see below. What else can asyncio do? ========================= Asynchronous IO is a really powerful tool, as we've seen. There are plenty of other useful libraries that also use `asyncio` and that you can integrate with Telethon. * `aiohttp `_ is like the infamous `requests `_ but asynchronous. * `quart `_ is an asynchronous alternative to `Flask `_. * `aiocron `_ lets you schedule things to run things at a desired time, or run some tasks hourly, daily, etc. And of course, `asyncio `_ itself! It has a lot of methods that let you do nice things. For example, you can run requests in parallel: .. code-block:: python async def main(): last, sent, download_path = await asyncio.gather( client.get_messages('telegram', 10), client.send_message('me', 'Using asyncio!'), client.download_profile_photo('telegram') ) loop.run_until_complete(main()) This code will get the 10 last messages from `@telegram `_, send one to the chat with yourself, and also download the profile photo of the channel. `asyncio` will run all these three tasks at the same time. You can run all the tasks you want this way. A different way would be: .. code-block:: python loop.create_task(client.get_messages('telegram', 10)) loop.create_task(client.send_message('me', 'Using asyncio!')) loop.create_task(client.download_profile_photo('telegram')) They will run in the background as long as the loop is running too. You can also `start an asyncio server `_ in the main script, and from another script, `connect to it `_ to achieve `Inter-Process Communication `_. You can get as creative as you want. You can program anything you want. When you use a library, you're not limited to use only its methods. You can combine all the libraries you want. People seem to forget this simple fact! Why does client.start() work outside async? =========================================== Because it's so common that it's really convenient to offer said functionality by default. This means you can set up all your event handlers and start the client without worrying about loops at all. Using the client in a ``with`` block, `start `, `run_until_disconnected `, and `disconnect ` all support this. Where can I read more? ====================== `Check out my blog post `_ about `asyncio`, which has some more examples and pictures to help you understand what happens when the loop runs. Telethon-1.39.0/readthedocs/concepts/botapi-vs-mtproto.rst000066400000000000000000000241351475566265000236560ustar00rootroot00000000000000.. _botapi: ======================= HTTP Bot API vs MTProto ======================= Telethon is more than just another viable alternative when developing bots for Telegram. If you haven't decided which wrapper library for bots to use yet, using Telethon from the beginning may save you some headaches later. .. contents:: What is Bot API? ================ The `Telegram Bot API`_, also known as HTTP Bot API and from now on referred to as simply "Bot API" is Telegram's official way for developers to control their own Telegram bots. Quoting their main page: The Bot API is an HTTP-based interface created for developers keen on building bots for Telegram. To learn how to create and set up a bot, please consult our `Introduction to Bots`_ and `Bot FAQ`_. Bot API is simply an HTTP endpoint which translates your requests to it into MTProto calls through tdlib_, their bot backend. Configuration of your bot, such as its available commands and auto-completion, is configured through `@BotFather `_. What is MTProto? ================ MTProto_ is Telegram's own protocol to communicate with their API when you connect to their servers. Telethon is an alternative MTProto-based backend written entirely in Python and much easier to setup and use. Both official applications and third-party clients (like your own applications) logged in as either user or bots **can use MTProto** to communicate directly with Telegram's API (which is not the HTTP bot API). When we talk about MTProto, we often mean "MTProto-based clients". Advantages of MTProto over Bot API ================================== MTProto clients (like Telethon) connect directly to Telegram's servers, which means there is no HTTP connection, no "polling" or "web hooks". This means **less overhead**, since the protocol used between you and the server is much more compact than HTTP requests with responses in wasteful JSON. Since there is a direct connection to Telegram's servers, even if their Bot API endpoint is down, you can still have connection to Telegram directly. Using a MTProto client, you are also not limited to the public API that they expose, and instead, **you have full control** of what your bot can do. Telethon offers you all the power with often **much easier usage** than any of the available Python Bot API wrappers. If your application ever needs user features because bots cannot do certain things, you will be able to easily login as a user and even keep your bot without having to learn a new library. If less overhead and full control didn't convince you to use Telethon yet, check out the wiki page `MTProto vs HTTP Bot API`_ with a more exhaustive and up-to-date list of differences. Migrating from Bot API to Telethon ================================== It doesn't matter if you wrote your bot with requests_ and you were making API requests manually, or if you used a wrapper library like python-telegram-bot_ or pyTelegramBotAPI_. It's never too late to migrate to Telethon! If you were using an asynchronous library like aiohttp_ or a wrapper like aiogram_ or dumbot_, it will be even easier, because Telethon is also an asynchronous library. Next, we will see some examples from the most popular libraries. Migrating from python-telegram-bot ---------------------------------- Let's take their `echobot.py`_ example and shorten it a bit: .. code-block:: python from telegram.ext import Updater, CommandHandler, MessageHandler, Filters def start(update, context): """Send a message when the command /start is issued.""" update.message.reply_text('Hi!') def echo(update, context): """Echo the user message.""" update.message.reply_text(update.message.text) def main(): """Start the bot.""" updater = Updater("TOKEN") dp = updater.dispatcher dp.add_handler(CommandHandler("start", start)) dp.add_handler(MessageHandler(Filters.text & ~Filters.command, echo)) updater.start_polling() updater.idle() if __name__ == '__main__': main() After using Telethon: .. code-block:: python from telethon import TelegramClient, events bot = TelegramClient('bot', 11111, 'a1b2c3d4').start(bot_token='TOKEN') @bot.on(events.NewMessage(pattern='/start')) async def start(event): """Send a message when the command /start is issued.""" await event.respond('Hi!') raise events.StopPropagation @bot.on(events.NewMessage) async def echo(event): """Echo the user message.""" await event.respond(event.text) def main(): """Start the bot.""" bot.run_until_disconnected() if __name__ == '__main__': main() Key differences: * The recommended way to do it imports fewer things. * All handlers trigger by default, so we need ``events.StopPropagation``. * Adding handlers, responding and running is a lot less verbose. * Telethon needs ``async def`` and ``await``. * The ``bot`` isn't hidden away by ``Updater`` or ``Dispatcher``. Migrating from pyTelegramBotAPI ------------------------------- Let's show another echobot from their README: .. code-block:: python import telebot bot = telebot.TeleBot("TOKEN") @bot.message_handler(commands=['start']) def send_welcome(message): bot.reply_to(message, "Howdy, how are you doing?") @bot.message_handler(func=lambda m: True) def echo_all(message): bot.reply_to(message, message.text) bot.polling() Now we rewrite it to use Telethon: .. code-block:: python from telethon import TelegramClient, events bot = TelegramClient('bot', 11111, 'a1b2c3d4').start(bot_token='TOKEN') @bot.on(events.NewMessage(pattern='/start')) async def send_welcome(event): await event.reply('Howdy, how are you doing?') @bot.on(events.NewMessage) async def echo_all(event): await event.reply(event.text) bot.run_until_disconnected() Key differences: * Instead of doing ``bot.reply_to(message)``, we can do ``event.reply``. Note that the ``event`` behaves just like their ``message``. * Telethon also supports ``func=lambda m: True``, but it's not necessary. Migrating from aiogram ---------------------- From their GitHub: .. code-block:: python from aiogram import Bot, Dispatcher, executor, types API_TOKEN = 'BOT TOKEN HERE' # Initialize bot and dispatcher bot = Bot(token=API_TOKEN) dp = Dispatcher(bot) @dp.message_handler(commands=['start']) async def send_welcome(message: types.Message): """ This handler will be called when client send `/start` command. """ await message.reply("Hi!\nI'm EchoBot!\nPowered by aiogram.") @dp.message_handler(regexp='(^cat[s]?$|puss)') async def cats(message: types.Message): with open('data/cats.jpg', 'rb') as photo: await bot.send_photo(message.chat.id, photo, caption='Cats is here 😺', reply_to_message_id=message.message_id) @dp.message_handler() async def echo(message: types.Message): await bot.send_message(message.chat.id, message.text) if __name__ == '__main__': executor.start_polling(dp, skip_updates=True) After rewrite: .. code-block:: python from telethon import TelegramClient, events # Initialize bot and... just the bot! bot = TelegramClient('bot', 11111, 'a1b2c3d4').start(bot_token='TOKEN') @bot.on(events.NewMessage(pattern='/start')) async def send_welcome(event): await event.reply('Howdy, how are you doing?') @bot.on(events.NewMessage(pattern='(^cat[s]?$|puss)')) async def cats(event): await event.reply('Cats is here 😺', file='data/cats.jpg') @bot.on(events.NewMessage) async def echo_all(event): await event.reply(event.text) if __name__ == '__main__': bot.run_until_disconnected() Key differences: * Telethon offers convenience methods to avoid retyping ``bot.send_photo(message.chat.id, ...)`` all the time, and instead let you type ``event.reply``. * Sending files is **a lot** easier. The methods for sending photos, documents, audios, etc. are all the same! Migrating from dumbot --------------------- Showcasing their subclassing example: .. code-block:: python from dumbot import Bot class Subbot(Bot): async def init(self): self.me = await self.getMe() async def on_update(self, update): await self.sendMessage( chat_id=update.message.chat.id, text='i am {}'.format(self.me.username) ) Subbot(token).run() After rewriting: .. code-block:: python from telethon import TelegramClient, events class Subbot(TelegramClient): def __init__(self, *a, **kw): super().__init__(*a, **kw) self.add_event_handler(self.on_update, events.NewMessage) async def connect(): await super().connect() self.me = await self.get_me() async def on_update(event): await event.reply('i am {}'.format(self.me.username)) bot = Subbot('bot', 11111, 'a1b2c3d4').start(bot_token='TOKEN') bot.run_until_disconnected() Key differences: * Telethon method names are ``snake_case``. * dumbot does not offer friendly methods like ``update.reply``. * Telethon does not have an implicit ``on_update`` handler, so we need to manually register one. .. _Telegram Bot API: https://core.telegram.org/bots/api .. _Introduction to Bots: https://core.telegram.org/bots .. _Bot FAQ: https://core.telegram.org/bots/faq .. _tdlib: https://core.telegram.org/tdlib .. _MTProto: https://core.telegram.org/mtproto .. _MTProto vs HTTP Bot API: https://github.com/LonamiWebs/Telethon/wiki/MTProto-vs-HTTP-Bot-API .. _requests: https://pypi.org/project/requests/ .. _python-telegram-bot: https://python-telegram-bot.readthedocs.io .. _pyTelegramBotAPI: https://github.com/eternnoir/pyTelegramBotAPI .. _aiohttp: https://docs.aiohttp.org/en/stable .. _aiogram: https://aiogram.readthedocs.io .. _dumbot: https://github.com/Lonami/dumbot .. _echobot.py: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/echobot.py Telethon-1.39.0/readthedocs/concepts/chats-vs-channels.rst000066400000000000000000000133131475566265000235650ustar00rootroot00000000000000.. _chats-channels: ================= Chats vs Channels ================= Telegram's raw API can get very confusing sometimes, in particular when it comes to talking about "chats", "channels", "groups", "megagroups", and all those concepts. This section will try to explain what each of these concepts are. Chats ===== A ``Chat`` can be used to talk about either the common "subclass" that both chats and channels share, or the concrete :tl:`Chat` type. Technically, both :tl:`Chat` and :tl:`Channel` are a form of the `Chat type`_. **Most of the time**, the term :tl:`Chat` is used to talk about *small group chats*. When you create a group through an official application, this is the type that you get. Official applications refer to these as "Group". Both the bot API and Telethon will add a minus sign (negate) the real chat ID so that you can tell at a glance, with just a number, the entity type. For example, if you create a chat with :tl:`CreateChatRequest`, the real chat ID might be something like `123`. If you try printing it from a `message.chat_id` you will see `-123`. This ID helps Telethon know you're talking about a :tl:`Chat`. Channels ======== Official applications create a *broadcast* channel when you create a new channel (used to broadcast messages, only administrators can post messages). Official applications implicitly *migrate* an *existing* :tl:`Chat` to a *megagroup* :tl:`Channel` when you perform certain actions (exceed user limit, add a public username, set certain permissions, etc.). A ``Channel`` can be created directly with :tl:`CreateChannelRequest`, as either a ``megagroup`` or ``broadcast``. Official applications use the term "channel" **only** for broadcast channels. The API refers to the different types of :tl:`Channel` with certain attributes: * A **broadcast channel** is a :tl:`Channel` with the ``channel.broadcast`` attribute set to `True`. * A **megagroup channel** is a :tl:`Channel` with the ``channel.megagroup`` attribute set to `True`. Official applications refer to this as "supergroup". * A **gigagroup channel** is a :tl:`Channel` with the ``channel.gigagroup`` attribute set to `True`. Official applications refer to this as "broadcast groups", and is used when a megagroup becomes very large and administrators want to transform it into something where only they can post messages. Both the bot API and Telethon will "concatenate" ``-100`` to the real chat ID so that you can tell at a glance, with just a number, the entity type. For example, if you create a new broadcast channel, the real channel ID might be something like `456`. If you try printing it from a `message.chat_id` you will see `-1000000000456`. This ID helps Telethon know you're talking about a :tl:`Channel`. Converting IDs ============== You can convert between the "marked" identifiers (prefixed with a minus sign) and the real ones with ``utils.resolve_id``. It will return a tuple with the real ID, and the peer type (the class): .. code-block:: python from telethon import utils real_id, peer_type = utils.resolve_id(-1000000000456) print(real_id) # 456 print(peer_type) # peer = peer_type(real_id) print(peer) # PeerChannel(channel_id=456) The reverse operation can be done with ``utils.get_peer_id``: .. code-block:: python print(utils.get_peer_id(types.PeerChannel(456))) # -1000000000456 Note that this function can also work with other types, like :tl:`Chat` or :tl:`Channel` instances. If you need to convert other types like usernames which might need to perform API calls to find out the identifier, you can use ``client.get_peer_id``: .. code-block:: python print(await client.get_peer_id('me')) # your id If there is no "mark" (no minus sign), Telethon will assume your identifier refers to a :tl:`User`. If this is **not** the case, you can manually fix it: .. code-block:: python from telethon import types await client.send_message(types.PeerChannel(456), 'hello') # ^^^^^^^^^^^^^^^^^ explicit peer type A note on raw API ================= Certain methods only work on a :tl:`Chat`, and some others only work on a :tl:`Channel` (and these may only work in broadcast, or megagroup). Your code likely knows what it's working with, so it shouldn't be too much of an issue. If you need to find the :tl:`Channel` from a :tl:`Chat` that migrated to it, access the `migrated_to` property: .. code-block:: python # chat is a Chat channel = await client.get_entity(chat.migrated_to) # channel is now a Channel Channels do not have a "migrated_from", but a :tl:`ChannelFull` does. You can use :tl:`GetFullChannelRequest` to obtain this: .. code-block:: python from telethon import functions full = await client(functions.channels.GetFullChannelRequest(your_channel)) full_channel = full.full_chat # full_channel is a ChannelFull print(full_channel.migrated_from_chat_id) This way, you can also access the linked discussion megagroup of a broadcast channel: .. code-block:: python print(full_channel.linked_chat_id) # prints ID of linked discussion group or None You do not need to use ``client.get_entity`` to access the ``migrated_from_chat_id`` :tl:`Chat` or the ``linked_chat_id`` :tl:`Channel`. They are in the ``full.chats`` attribute: .. code-block:: python if full_channel.migrated_from_chat_id: migrated_from_chat = next(c for c in full.chats if c.id == full_channel.migrated_from_chat_id) print(migrated_from_chat.title) if full_channel.linked_chat_id: linked_group = next(c for c in full.chats if c.id == full_channel.linked_chat_id) print(linked_group.username) .. _Chat type: https://tl.telethon.dev/types/chat.html Telethon-1.39.0/readthedocs/concepts/entities.rst000066400000000000000000000300741475566265000220730ustar00rootroot00000000000000.. _entities: ======== Entities ======== The library widely uses the concept of "entities". An entity will refer to any :tl:`User`, :tl:`Chat` or :tl:`Channel` object that the API may return in response to certain methods, such as :tl:`GetUsersRequest`. .. note:: When something "entity-like" is required, it means that you need to provide something that can be turned into an entity. These things include, but are not limited to, usernames, exact titles, IDs, :tl:`Peer` objects, or even entire :tl:`User`, :tl:`Chat` and :tl:`Channel` objects and even phone numbers **from people you have in your contact list**. To "encounter" an ID, you would have to "find it" like you would in the normal app. If the peer is in your dialogs, you would need to `client.get_dialogs() `. If the peer is someone in a group, you would similarly `client.get_participants(group) `. Once you have encountered an ID, the library will (by default) have saved their ``access_hash`` for you, which is needed to invoke most methods. This is why sometimes you might encounter this error when working with the library. You should ``except ValueError`` and run code that you know should work to find the entity. .. contents:: What is an Entity? ================== A lot of methods and requests require *entities* to work. For example, you send a message to an *entity*, get the username of an *entity*, and so on. There are a lot of things that work as entities: usernames, phone numbers, chat links, invite links, IDs, and the types themselves. That is, you can use any of those when you see an "entity" is needed. .. note:: Remember that the phone number must be in your contact list before you can use it. You should use, **from better to worse**: 1. Input entities. For example, `event.input_chat `, `message.input_sender `, or caching an entity you will use a lot with ``entity = await client.get_input_entity(...)``. 2. Entities. For example, if you had to get someone's username, you can just use ``user`` or ``channel``. It will work. Only use this option if you already have the entity! 3. IDs. This will always look the entity up from the cache (the ``*.session`` file caches seen entities). 4. Usernames, phone numbers and links. The cache will be used too (unless you force a `client.get_entity() `), but may make a request if the username, phone or link has not been found yet. In recent versions of the library, the following two are equivalent: .. code-block:: python async def handler(event): await client.send_message(event.sender_id, 'Hi') await client.send_message(event.input_sender, 'Hi') If you need to be 99% sure that the code will work (sometimes it's simply impossible for the library to find the input entity), or if you will reuse the chat a lot, consider using the following instead: .. code-block:: python async def handler(event): # This method may make a network request to find the input sender. # Properties can't make network requests, so we need a method. sender = await event.get_input_sender() await client.send_message(sender, 'Hi') await client.send_message(sender, 'Hi') Getting Entities ================ Through the use of the :ref:`sessions`, the library will automatically remember the ID and hash pair, along with some extra information, so you're able to just do this: .. code-block:: python # (These examples assume you are inside an "async def") # # Dialogs are the "conversations you have open". # This method returns a list of Dialog, which # has the .entity attribute and other information. # # This part is IMPORTANT, because it fills the entity cache. dialogs = await client.get_dialogs() # All of these work and do the same. username = await client.get_entity('username') username = await client.get_entity('t.me/username') username = await client.get_entity('https://telegram.dog/username') # Other kind of entities. channel = await client.get_entity('telegram.me/joinchat/AAAAAEkk2WdoDrB4-Q8-gg') contact = await client.get_entity('+34xxxxxxxxx') friend = await client.get_entity(friend_id) # Getting entities through their ID (User, Chat or Channel) entity = await client.get_entity(some_id) # You can be more explicit about the type for said ID by wrapping # it inside a Peer instance. This is recommended but not necessary. from telethon.tl.types import PeerUser, PeerChat, PeerChannel my_user = await client.get_entity(PeerUser(some_id)) my_chat = await client.get_entity(PeerChat(some_id)) my_channel = await client.get_entity(PeerChannel(some_id)) .. note:: You **don't** need to get the entity before using it! Just let the library do its job. Use a phone from your contacts, username, ID or input entity (preferred but not necessary), whatever you already have. All methods in the :ref:`telethon-client` call `.get_input_entity() ` prior to sending the request to save you from the hassle of doing so manually. That way, convenience calls such as `client.send_message('username', 'hi!') ` become possible. Every entity the library encounters (in any response to any call) will by default be cached in the ``.session`` file (an SQLite database), to avoid performing unnecessary API calls. If the entity cannot be found, additonal calls like :tl:`ResolveUsernameRequest` or :tl:`GetContactsRequest` may be made to obtain the required information. Entities vs. Input Entities =========================== .. note:: This section is informative, but worth reading. The library will transparently handle all of these details for you. On top of the normal types, the API also make use of what they call their ``Input*`` versions of objects. The input version of an entity (e.g. :tl:`InputPeerUser`, :tl:`InputChat`, etc.) only contains the minimum information that's required from Telegram to be able to identify who you're referring to: a :tl:`Peer`'s **ID** and **hash**. They are named like this because they are input parameters in the requests. Entities' ID are the same for all user and bot accounts, however, the access hash is **different for each account**, so trying to reuse the access hash from one account in another will **not** work. Sometimes, Telegram only needs to indicate the type of the entity along with their ID. For this purpose, :tl:`Peer` versions of the entities also exist, which just have the ID. You cannot get the hash out of them since you should not be needing it. The library probably has cached it before. Peers are enough to identify an entity, but they are not enough to make a request with them. You need to know their hash before you can "use them", and to know the hash you need to "encounter" them, let it be in your dialogs, participants, message forwards, etc. .. note:: You *can* use peers with the library. Behind the scenes, they are replaced with the input variant. Peers "aren't enough" on their own but the library will do some more work to use the right type. As we just mentioned, API calls don't need to know the whole information about the entities, only their ID and hash. For this reason, another method, `client.get_input_entity() ` is available. This will always use the cache while possible, making zero API calls most of the time. When a request is made, if you provided the full entity, e.g. an :tl:`User`, the library will convert it to the required :tl:`InputPeer` automatically for you. **You should always favour** `client.get_input_entity() ` **over** `client.get_entity() ` for this reason! Calling the latter will always make an API call to get the most recent information about said entity, but invoking requests don't need this information, just the :tl:`InputPeer`. Only use `client.get_entity() ` if you need to get actual information, like the username, name, title, etc. of the entity. To further simplify the workflow, since the version ``0.16.2`` of the library, the raw requests you make to the API are also able to call `client.get_input_entity() ` wherever needed, so you can even do things like: .. code-block:: python await client(SendMessageRequest('username', 'hello')) The library will call the ``.resolve()`` method of the request, which will resolve ``'username'`` with the appropriated :tl:`InputPeer`. Don't worry if you don't get this yet, but remember some of the details here are important. Full Entities ============= In addition to :tl:`PeerUser`, :tl:`InputPeerUser`, :tl:`User` (and its variants for chats and channels), there is also the concept of :tl:`UserFull`. This full variant has additional information such as whether the user is blocked, its notification settings, the bio or about of the user, etc. There is also :tl:`messages.ChatFull` which is the equivalent of full entities for chats and channels, with also the about section of the channel. Note that the ``users`` field only contains bots for the channel (so that clients can suggest commands to use). You can get both of these by invoking :tl:`GetFullUser`, :tl:`GetFullChat` and :tl:`GetFullChannel` respectively. Accessing Entities ================== Although it's explicitly noted in the documentation that messages *subclass* `ChatGetter ` and `SenderGetter `, some people still don't get inheritance. When the documentation says "Bases: `telethon.tl.custom.chatgetter.ChatGetter`" it means that the class you're looking at, *also* can act as the class it bases. In this case, `ChatGetter ` knows how to get the *chat* where a thing belongs to. So, a `Message ` is a `ChatGetter `. That means you can do this: .. code-block:: python message.is_private message.chat_id await message.get_chat() # ...etc `SenderGetter ` is similar: .. code-block:: python message.user_id await message.get_input_sender() message.user # ...etc Quite a few things implement them, so it makes sense to reuse the code. For example, all events (except raw updates) implement `ChatGetter ` since all events occur in some chat. Summary ======= TL;DR; If you're here because of *"Could not find the input entity for"*, you must ask yourself "how did I find this entity through official applications"? Now do the same with the library. Use what applies: .. code-block:: python # (These examples assume you are inside an "async def") async with client: # Does it have a username? Use it! entity = await client.get_entity(username) # Do you have a conversation open with them? Get dialogs. await client.get_dialogs() # Are they participant of some group? Get them. await client.get_participants('username') # Is the entity the original sender of a forwarded message? Get it. await client.get_messages('username', 100) # NOW you can use the ID, anywhere! await client.send_message(123456, 'Hi!') entity = await client.get_entity(123456) print(entity) Once the library has "seen" the entity, you can use their **integer** ID. You can't use entities from IDs the library hasn't seen. You must make the library see them *at least once* and disconnect properly. You know where the entities are and you must tell the library. It won't guess for you. Telethon-1.39.0/readthedocs/concepts/errors.rst000066400000000000000000000123151475566265000215610ustar00rootroot00000000000000.. _rpc-errors: ========== RPC Errors ========== RPC stands for Remote Procedure Call, and when the library raises a ``RPCError``, it's because you have invoked some of the API methods incorrectly (wrong parameters, wrong permissions, or even something went wrong on Telegram's server). You should import the errors from ``telethon.errors`` like so: .. code-block:: python from telethon import errors try: async with client.takeout() as takeout: ... except errors.TakeoutInitDelayError as e: # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ here we except TAKEOUT_INIT_DELAY print('Must wait', e.seconds, 'before takeout') There isn't any official list of all possible RPC errors, so the `list of known errors`_ is provided on a best-effort basis. When new methods are available, the list may be lacking since we simply don't know what errors can raise from them. Once we do find out about a new error and what causes it, the list is updated, so if you see an error without a specific class, do report it (and what method caused it)!. This list is used to generate documentation for the `raw API page`_. For example, if we want to know what errors can occur from `messages.sendMessage`_ we can simply navigate to its raw API page and find it has 24 known RPC errors at the time of writing. Base Errors =========== All the "base" errors are listed in :ref:`telethon-errors`. Any other more specific error will be a subclass of these. If the library isn't aware of a specific error just yet, it will instead raise one of these superclasses. This means you may find stuff like this: .. code-block:: text telethon.errors.rpcbaseerrors.BadRequestError: RPCError 400: MESSAGE_POLL_CLOSED (caused by SendVoteRequest) If you do, make sure to open an issue or send a pull request to update the `list of known errors`_. Common Errors ============= These are some of the errors you may normally need to deal with: - ``FloodWaitError`` (420), the same request was repeated many times. Must wait ``.seconds`` (you can access this attribute). For example: .. code-block:: python ... from telethon import errors try: messages = await client.get_messages(chat) print(messages[0].text) except errors.FloodWaitError as e: print('Have to sleep', e.seconds, 'seconds') time.sleep(e.seconds) - ``SessionPasswordNeededError``, if you have setup two-steps verification on Telegram and are trying to sign in. - ``FilePartMissingError``, if you have tried to upload an empty file. - ``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``. You can refer to all errors from Python through the ``telethon.errors`` module. If you don't know what attributes they have, try printing their dir (like ``print(dir(e))``). Attributes ========== Some of the errors carry additional data in them. When they look like ``EMAIL_UNCONFIRMED_X``, the ``_X`` value will be accessible from the error instance. The current list of errors that do this is the following: - ``EmailUnconfirmedError`` has ``.code_length``. - ``FileMigrateError`` has ``.new_dc``. - ``FilePartMissingError`` has ``.which``. - ``FloodTestPhoneWaitError`` has ``.seconds``. - ``FloodWaitError`` has ``.seconds``. - ``InterdcCallErrorError`` has ``.dc``. - ``InterdcCallRichErrorError`` has ``.dc``. - ``NetworkMigrateError`` has ``.new_dc``. - ``PhoneMigrateError`` has ``.new_dc``. - ``SlowModeWaitError`` has ``.seconds``. - ``TakeoutInitDelayError`` has ``.seconds``. - ``UserMigrateError`` has ``.new_dc``. Avoiding Limits =============== Don't spam. You won't get ``FloodWaitError`` or your account banned or deleted if you use the library *for legit use cases*. Make cool tools. Don't spam! Nobody knows the exact limits for all requests since they depend on a lot of factors, so don't bother asking. Still, if you do have a legit use case and still get those errors, the library will automatically sleep when they are smaller than 60 seconds by default. You can set different "auto-sleep" thresholds: .. code-block:: python client.flood_sleep_threshold = 0 # Don't auto-sleep client.flood_sleep_threshold = 24 * 60 * 60 # Sleep always You can also except it and act as you prefer: .. code-block:: python from telethon.errors import FloodWaitError try: ... except FloodWaitError as e: print('Flood waited for', e.seconds) quit(1) VoIP numbers are very limited, and some countries are more limited too. .. _list of known errors: https://github.com/LonamiWebs/Telethon/blob/v1/telethon_generator/data/errors.csv .. _raw API page: https://tl.telethon.dev/ .. _messages.sendMessage: https://tl.telethon.dev/methods/messages/send_message.html Telethon-1.39.0/readthedocs/concepts/full-api.rst000066400000000000000000000351271475566265000217640ustar00rootroot00000000000000.. _full-api: ============ The Full API ============ .. important:: While you have access to this, you should always use the friendly methods listed on :ref:`client-ref` unless you have a better reason not to, like a method not existing or you wanting more control. .. contents:: Introduction ============ The :ref:`telethon-client` doesn't offer a method for every single request the Telegram API supports. However, it's very simple to *call* or *invoke* any request defined in Telegram's API. This section will teach you how to use what Telethon calls the `TL reference`_. The linked page contains a list and a way to search through *all* types generated from the definition of Telegram's API (in ``.tl`` file format, hence the name). These types include requests and constructors. .. note:: The reason to keep both https://tl.telethon.dev and this documentation alive is that the former allows instant search results as you type, and a "Copy import" button. If you like namespaces, you can also do ``from telethon.tl import types, functions``. Both work. Telegram makes these ``.tl`` files public, which other implementations, such as Telethon, can also use to generate code. These files are versioned under what's called "layers". ``.tl`` files consist of thousands of definitions, and newer layers often add, change, or remove them. Each definition refers to either a Remote Procedure Call (RPC) function, or a type (which the `TL reference`_ calls "constructors", as they construct particular type instances). As such, the `TL reference`_ is a good place to go to learn about all possible requests, types, and what they look like. If you're curious about what's been changed between layers, you can refer to the `TL diff`_ site. Navigating the TL reference =========================== Functions --------- "Functions" is the term used for the Remote Procedure Calls (RPC) that can be sent to Telegram to ask it to perform something (e.g. "send message"). These requests have an associated return type. These can be invoked ("called"): .. code-block:: python client = TelegramClient(...) function_instance = SomeRequest(...) # Invoke the request returned_type = await client(function_instance) Whenever you find the type for a function in the `TL reference`_, the page will contain the following information: * What type of account can use the method. This information is regenerated from time to time (by attempting to invoke the function under both account types and finding out where it fails). Some requests can only be used by bot accounts, others by user accounts, and others by both. * The TL definition. This helps you get a feel for the what the function looks like. This is not Python code. It just contains the definition in a concise manner. * "Copy import" button. Does what it says: it will copy the necessary Python code to import the function to your system's clipboard for easy access. * Returns. The returned type. When you invoke the function, this is what the result will be. It also includes which of the constructors can be returned inline, to save you a click. * Parameters. The parameters accepted by the function, including their type, whether they expect a list, and whether they're optional. * Known RPC errors. A best-effort list of known errors the request may cause. This list is not complete and may be out of date, but should provide an overview of what could go wrong. * Example. Autogenerated example, showcasing how you may want to call it. Bear in mind that this is *autogenerated*. It may be spitting out non-sense. The goal of this example is not to show you everything you can do with the request, only to give you a feel for what it looks like to use it. It is very important to click through the links and navigate to get the full picture. A specific page will show you what the specific function returns and needs as input parameters. But it may reference other types, so you need to navigate to those to learn what those contain or need. Types ----- "Types" as understood by TL are not actually generated in Telethon. They would be the "abstract base class" of the constructors, but since Python is duck-typed, there is hardly any need to generate mostly unnecessary code. The page for a type contains: * Constructors. Every type will have one or more constructors. These constructors *are* generated and can be immported and used. * Requests returning this type. A helpful way to find out "what requests can return this?". This is how you may learn what request you need to use to obtain a particular instance of a type. * Requests accepting this type as input. A helpful way to find out "what requests can use this type as one of their input parameters?". This is how you may learn where a type is used. * Other types containing this type. A helpful way to find out "where else does this type appear?". This is how you can walk back through nested objects. Constructors ------------ Constructors are used to create instances of a particular type, and are also returned when invoking requests. You will have to create instances yourself when invoking requests that need a particular type as input. The page for a constructor contains: * Belongs to. The parent type. This is a link back to the types page for the specific constructor. It also contains the sibling constructors inline, to save you a click. * Members. Both the input parameters *and* fields the constructor contains. Using the TL reference ====================== After you've found a request you want to send, a good start would be to simply copy and paste the autogenerated example into your script. Then you can simply tweak it to your needs. If you want to do it from scratch, first, make sure to import the request into your code (either using the "Copy import" button near the top, or by manually spelling out the package under ``telethon.tl.functions.*``). Then, start reading the parameters one by one. If the parameter cannot be omitted, you **will** need to specify it, so make sure to spell it out as an input parameter when constructing the request instance. Let's look at `PingRequest`_ for example. First, we copy the import: .. code-block:: python from telethon.tl.functions import PingRequest Then, we look at the parameters: ping_id - long A single parameter, and it's a long (a integer number with a large range of values). It doesn't say it can be omitted, so we must provide it, like so: .. code-block:: python PingRequest( ping_id=48641868471 ) (In this case, the ping ID is a random number. You often have to guess what the parameter needs just by looking at the name.) Now that we have our request, we can invoke it: .. code-block:: python response = await client(PingRequest( ping_id=48641868471 )) To find out what ``response`` looks like, we can do as the autogenerated example suggests and "stringify" the result as a pretty-printed string: .. code-block:: python print(result.stringify()) This will print out the following: .. code-block:: python Pong( msg_id=781875678118, ping_id=48641868471 ) Which is a very easy way to get a feel for a response. You should nearly always print the stringified result, at least once, when trying out requests, to get a feel for what the response may look like. But of course, you don't need to do that. Without writing any code, you could have navigated through the "Returns" link to learn ``PingRequest`` returns a ``Pong``, which only has one constructor, and the constructor has two members, ``msg_id`` and ``ping_id``. If you wanted to create your own ``Pong``, you would use both members as input parameters: .. code-block:: python my_pong = Pong( msg_id=781875678118, ping_id=48641868471 ) (Yes, constructing object instances can use the same code that ``.stringify`` would return!) And if you wanted to access the ``msg_id`` member, you would simply access it like any other attribute access in Python: .. code-block:: python print(response.msg_id) Example walkthrough =================== Say `client.send_message() ` didn't exist, we could `use the search`_ to look for "message". There we would find :tl:`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 from telethon.tl import types, functions # We now have access to 'functions.messages.SendMessageRequest' We see that this request must take at least two parameters, a ``peer`` of type :tl:`InputPeer`, and a ``message`` which is just a Python `str`\ ing. How can we retrieve this :tl:`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 `client.get_input_entity() `: .. code-block:: python import telethon async def main(): peer = await client.get_input_entity('someone') client.loop.run_until_complete(main()) .. note:: Remember that ``await`` must occur inside an ``async def``. Every full API example assumes you already know and do this. When you're going to invoke an API method, most require you to pass an :tl:`InputUser`, :tl:`InputChat`, or so on, this is why using `client.get_input_entity() ` is more straightforward (and often immediate, if you've seen the user before, know their ID, etc.). If you also **need** to have information about the whole user, use `client.get_entity() ` instead: .. code-block:: python entity = await 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 `telethon.utils.get_input_peer`: .. code-block:: python from telethon import utils peer = utils.get_input_peer(entity) .. note:: Since ``v0.16.2`` this is further simplified. The ``Request`` itself will call `client.get_input_entity ` for you when required, but it's good to remember what's happening. After this small parenthesis about `client.get_entity ` versus `client.get_input_entity() `, we have everything we need. To invoke our request we do: .. code-block:: python result = await client(SendMessageRequest(peer, 'Hello there!')) Message sent! Of course, this is only an example. There are over 250 methods available as of layer 80, and you can use every single of them as you wish. Remember to use the right types! To sum up: .. code-block:: python result = await client(SendMessageRequest( await client.get_input_entity('username'), 'Hello there!' )) This can further be simplified to: .. code-block:: python result = await client(SendMessageRequest('username', 'Hello there!')) # Or even result = await client(SendMessageRequest(PeerChannel(id), '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 default amount". This won't work for all of them though, for instance, in "messages.search" it will actually return 0 items. Requests in Parallel ==================== The library will automatically merge outgoing requests into a single *container*. Telegram's API supports sending multiple requests in a single container, which is faster because it has less overhead and the server can run them without waiting for others. You can also force using a container manually: .. code-block:: python async def main(): # Letting the library do it behind the scenes await asyncio.wait([ client.send_message('me', 'Hello'), client.send_message('me', ','), client.send_message('me', 'World'), client.send_message('me', '.') ]) # Manually invoking many requests at once await client([ SendMessageRequest('me', 'Hello'), SendMessageRequest('me', ', '), SendMessageRequest('me', 'World'), SendMessageRequest('me', '.') ]) Note that you cannot guarantee the order in which they are run. Try running the above code more than one time. You will see the order in which the messages arrive is different. If you use the raw API (the first option), you can use ``ordered`` to tell the server that it should run the requests sequentially. This will still be faster than going one by one, since the server knows all requests directly: .. code-block:: python await client([ SendMessageRequest('me', 'Hello'), SendMessageRequest('me', ', '), SendMessageRequest('me', 'World'), SendMessageRequest('me', '.') ], ordered=True) If any of the requests fails with a Telegram error (not connection errors or any other unexpected events), the library will raise `telethon.errors.common.MultiError`. You can ``except`` this and still access the successful results: .. code-block:: python from telethon.errors import MultiError try: await client([ SendMessageRequest('me', 'Hello'), SendMessageRequest('me', ''), SendMessageRequest('me', 'World') ], ordered=True) except MultiError as e: # The first and third requests worked. first = e.results[0] third = e.results[2] # The second request failed. second = e.exceptions[1] .. _TL reference: https://tl.telethon.dev .. _TL diff: https://diff.telethon.dev .. _PingRequest: https://tl.telethon.dev/methods/ping.html .. _use the search: https://tl.telethon.dev/?q=message&redirect=no Telethon-1.39.0/readthedocs/concepts/sessions.rst000066400000000000000000000146251475566265000221210ustar00rootroot00000000000000.. _sessions: ============== Session Files ============== .. contents:: They are an important part for the library to be efficient, such as caching and handling your authorization key (or you would have to login every time!). What are Sessions? ================== The first parameter you pass to the constructor of the :ref:`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 in the working directory. Note that if you pass a string it will be a file in the current working directory, although you can also pass absolute paths. The session file contains enough information for you to login without re-sending the code, so if you have to enter the code more than once, maybe you're changing the working directory, renaming or removing the file, or using random names. These database files using ``sqlite3`` contain the required information to talk to the Telegram servers, such as to which IP the client should connect, port, authorization key so that messages can be encrypted, and so on. These files will by default also save all the input entities that you've seen, so that you can get information about a user or channel by just their ID. Telegram will **not** send their ``access_hash`` required to retrieve more information about them, if it thinks you have already seem them. For this reason, the library needs to store this information offline. The library will by default too save all the entities (chats and channels with their name and username, and users with the phone too) in the session file, so that you can quickly access them by username or phone number. If you're not going to work with updates, or don't need to cache the ``access_hash`` associated with the entities' ID, you can disable this by setting ``client.session.save_entities = False``. Different Session Storage ========================= If you don't want to use the default SQLite session storage, you can also use one of the other implementations or implement your own storage. While it's often not the case, it's possible that SQLite is slow enough to be noticeable, in which case you can also use a different storage. Note that this is rare and most people won't have this issue, but it's worth a mention. To use a custom session storage, simply pass the custom session instance to :ref:`TelegramClient ` instead of the session name. Telethon contains three implementations of the abstract ``Session`` class: .. currentmodule:: telethon.sessions * `MemorySession `: stores session data within memory. * `SQLiteSession `: stores sessions within on-disk SQLite databases. Default. * `StringSession `: stores session data within memory, but can be saved as a string. You can import these ``from telethon.sessions``. For example, using the `StringSession ` is done as follows: .. code-block:: python from telethon.sync import TelegramClient from telethon.sessions import StringSession with TelegramClient(StringSession(string), api_id, api_hash) as client: ... # use the client # Save the string session as a string; you should decide how # you want to save this information (over a socket, remote # database, print it and then paste the string in the code, # etc.); the advantage is that you don't need to save it # on the current disk as a separate file, and can be reused # anywhere else once you log in. string = client.session.save() # Note that it's also possible to save any other session type # as a string by using ``StringSession.save(session_instance)``: client = TelegramClient('sqlite-session', api_id, api_hash) string = StringSession.save(client.session) There are other community-maintained implementations available: * `SQLAlchemy `_: stores all sessions in a single database via SQLAlchemy. * `Redis `_: stores all sessions in a single Redis data store. * `MongoDB `_: stores the current session in a MongoDB database. Creating your Own Storage ========================= The easiest way to create your own storage implementation is to use `MemorySession ` as the base and check out how `SQLiteSession ` or one of the community-maintained implementations work. You can find the relevant Python files under the ``sessions/`` directory in the Telethon's repository. After you have made your own implementation, you can add it to the community-maintained session implementation list above with a pull request. String Sessions =============== `StringSession ` are a convenient way to embed your login credentials directly into your code for extremely easy portability, since all they take is a string to be able to login without asking for your phone and code (or faster start if you're using a bot token). The easiest way to generate a string session is as follows: .. code-block:: python from telethon.sync import TelegramClient from telethon.sessions import StringSession with TelegramClient(StringSession(), api_id, api_hash) as client: print(client.session.save()) Think of this as a way to export your authorization key (what's needed to login into your account). This will print a string in the standard output (likely your terminal). .. warning:: **Keep this string safe!** Anyone with this string can use it to login into your account and do anything they want to. This is similar to leaking your ``*.session`` files online, but it is easier to leak a string than it is to leak a file. Once you have the string (which is a bit long), load it into your script somehow. You can use a normal text file and ``open(...).read()`` it or you can save it in a variable directly: .. code-block:: python string = '1aaNk8EX-YRfwoRsebUkugFvht6DUPi_Q25UOCzOAqzc...' with TelegramClient(StringSession(string), api_id, api_hash) as client: client.loop.run_until_complete(client.send_message('me', 'Hi')) These strings are really convenient for using in places like Heroku since their ephemeral filesystem will delete external files once your application is over. Telethon-1.39.0/readthedocs/concepts/strings.rst000066400000000000000000000053331475566265000217400ustar00rootroot00000000000000====================== String-based Debugging ====================== Debugging is *really* important. Telegram's API is really big and there are a lot of things that you should know. Such as, what attributes or fields does a result have? Well, the easiest thing to do is printing it: .. code-block:: python entity = await client.get_entity('username') print(entity) That will show a huge **string** similar to the following: .. code-block:: python Channel(id=1066197625, title='Telegram Usernames', photo=ChatPhotoEmpty(), date=datetime.datetime(2016, 12, 16, 15, 15, 43, tzinfo=datetime.timezone.utc), version=0, creator=False, left=True, broadcast=True, verified=True, megagroup=False, restricted=False, signatures=False, min=False, scam=False, has_link=False, has_geo=False, slowmode_enabled=False, access_hash=-6309373984955162244, username='username', restriction_reason=[], admin_rights=None, banned_rights=None, default_banned_rights=None, participants_count=None) That's a lot of text. But as you can see, all the properties are there. So if you want the title you **don't use regex** or anything like splitting ``str(entity)`` to get what you want. You just access the attribute you need: .. code-block:: python title = entity.title Can we get better than the shown string, though? Yes! .. code-block:: python print(entity.stringify()) Will show a much better representation: .. code-block:: python Channel( id=1066197625, title='Telegram Usernames', photo=ChatPhotoEmpty( ), date=datetime.datetime(2016, 12, 16, 15, 15, 43, tzinfo=datetime.timezone.utc), version=0, creator=False, left=True, broadcast=True, verified=True, megagroup=False, restricted=False, signatures=False, min=False, scam=False, has_link=False, has_geo=False, slowmode_enabled=False, access_hash=-6309373984955162244, username='username', restriction_reason=[ ], admin_rights=None, banned_rights=None, default_banned_rights=None, participants_count=None ) Now it's easy to see how we could get, for example, the ``year`` value. It's inside ``date``: .. code-block:: python channel_year = entity.date.year You don't need to print everything to see what all the possible values can be. You can just search in http://tl.telethon.dev/. Remember that you can use Python's `isinstance `_ to check the type of something. For example: .. code-block:: python from telethon import types if isinstance(entity.photo, types.ChatPhotoEmpty): print('Channel has no photo') Telethon-1.39.0/readthedocs/concepts/updates.rst000066400000000000000000000163771475566265000217260ustar00rootroot00000000000000================ Updates in Depth ================ Properties vs. Methods ====================== The event shown above acts just like a `custom.Message `, which means you can access all the properties it has, like ``.sender``. **However** events are different to other methods in the client, like `client.get_messages `. Events *may not* send information about the sender or chat, which means it can be `None`, but all the methods defined in the client always have this information so it doesn't need to be re-fetched. For this reason, you have ``get_`` methods, which will make a network call if necessary. In short, you should do this: .. code-block:: python @client.on(events.NewMessage) async def handler(event): # event.input_chat may be None, use event.get_input_chat() chat = await event.get_input_chat() sender = await event.get_sender() buttons = await event.get_buttons() async def main(): async for message in client.iter_messages('me', 10): # Methods from the client always have these properties ready chat = message.input_chat sender = message.sender buttons = message.buttons Notice, properties (`message.sender `) don't need an ``await``, but methods (`message.get_sender `) **do** need an ``await``, and you should use methods in events for these properties that may need network. Events Without the client ========================= The code of your application starts getting big, so you decide to separate the handlers into different files. But how can you access the client from these files? You don't need to! Just `events.register ` them: .. code-block:: python # handlers/welcome.py from telethon import events @events.register(events.NewMessage('(?i)hello')) async def handler(event): client = event.client await event.respond('Hey!') await client.send_message('me', 'I said hello to someone') Registering events is a way of saying "this method is an event handler". You can use `telethon.events.is_handler` to check if any method is a handler. You can think of them as a different approach to Flask's blueprints. It's important to note that this does **not** add the handler to any client! You never specified the client on which the handler should be used. You only declared that it is a handler, and its type. To actually use the handler, you need to `client.add_event_handler ` to the client (or clients) where they should be added to: .. code-block:: python # main.py from telethon import TelegramClient import handlers.welcome with TelegramClient(...) as client: client.add_event_handler(handlers.welcome.handler) client.run_until_disconnected() This also means that you can register an event handler once and then add it to many clients without re-declaring the event. Events Without Decorators ========================= If for any reason you don't want to use `telethon.events.register`, you can explicitly pass the event handler to use to the mentioned `client.add_event_handler `: .. code-block:: python from telethon import TelegramClient, events async def handler(event): ... with TelegramClient(...) as client: client.add_event_handler(handler, events.NewMessage) client.run_until_disconnected() Similarly, you also have `client.remove_event_handler ` and `client.list_event_handlers `. The ``event`` argument is optional in all three methods and defaults to `events.Raw ` for adding, and `None` when removing (so all callbacks would be removed). .. note:: The ``event`` type is ignored in `client.add_event_handler ` if you have used `telethon.events.register` on the ``callback`` before, since that's the point of using such method at all. Stopping Propagation of Updates =============================== There might be cases when an event handler is supposed to be used solitary and it makes no sense to process any other handlers in the chain. For this case, it is possible to raise a `telethon.events.StopPropagation` exception which will cause the propagation of the update through your handlers to stop: .. code-block:: python from telethon.events import StopPropagation @client.on(events.NewMessage) async def _(event): # ... some conditions await event.delete() # Other handlers won't have an event to work with raise StopPropagation @client.on(events.NewMessage) async def _(event): # Will never be reached, because it is the second handler # in the chain. pass Remember to check :ref:`telethon-events` if you're looking for the methods reference. Understanding asyncio ===================== With `asyncio`, the library has several tasks running in the background. One task is used for sending requests, another task is used to receive them, and a third one is used to handle updates. To handle updates, you must keep your script running. You can do this in several ways. For instance, if you are *not* running `asyncio`'s event loop, you should use `client.run_until_disconnected `: .. code-block:: python import asyncio from telethon import TelegramClient client = TelegramClient(...) ... client.run_until_disconnected() Behind the scenes, this method is ``await``'ing on the `client.disconnected ` property, so the code above and the following are equivalent: .. code-block:: python import asyncio from telethon import TelegramClient client = TelegramClient(...) async def main(): await client.disconnected asyncio.run(main()) You could also run `client.disconnected ` until it completed. But if you don't want to ``await``, then you should know what you want to be doing instead! What matters is that you shouldn't let your script die. If you don't care about updates, you don't need any of this. Notice that unlike `client.disconnected `, `client.run_until_disconnected ` will handle ``KeyboardInterrupt`` for you. This method is special and can also be ran while the loop is running, so you can do this: .. code-block:: python async def main(): await client.run_until_disconnected() loop.run_until_complete(main()) Sequential Updates ================== If you need to process updates sequentially (i.e. not in parallel), you should set ``sequential_updates=True`` when creating the client: .. code-block:: python with TelegramClient(..., sequential_updates=True) as client: ... Telethon-1.39.0/readthedocs/conf.py000066400000000000000000000141431475566265000171750ustar00rootroot00000000000000#!/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 re import os import sys sys.path.insert(0, os.path.abspath(os.curdir)) sys.path.insert(0, os.path.abspath(os.pardir)) root = os.path.abspath(os.path.join(__file__, os.path.pardir, os.path.pardir)) tl_ref_url = 'https://tl.telethon.dev' # -- 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', 'sphinx.ext.autosummary', 'sphinx.ext.intersphinx', 'custom_roles' ] intersphinx_mapping = { 'python': ('https://docs.python.org/3', None) } # Change the default role so we can avoid prefixing everything with :obj: default_role = "py:obj" # 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 - 2019, 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. with open(os.path.join(root, 'telethon', 'version.py'), 'r') as f: version = re.search(r"^__version__\s+=\s+'(.*)'$", f.read(), flags=re.MULTILINE).group(1) # The full version, including alpha/beta/rc tags. release = version # 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 = 'en' # 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 = 'friendly' # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False def skip(app, what, name, obj, would_skip, options): if name.endswith('__'): # We want to show special methods names, except some which add clutter return name in { '__init__', '__abstractmethods__', '__module__', '__doc__', '__dict__' } return would_skip def setup(app): app.connect("autodoc-skip-member", skip) # -- 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', author, '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'), ] Telethon-1.39.0/readthedocs/custom_roles.py000066400000000000000000000040601475566265000207630ustar00rootroot00000000000000from docutils import nodes, utils from docutils.parsers.rst.roles import set_classes def make_link_node(rawtext, app, name, options): """ Create a link to the TL reference. :param rawtext: Text being replaced with link node. :param app: Sphinx application context :param name: Name of the object to link to :param options: Options dictionary passed to role func. """ try: base = app.config.tl_ref_url if not base: raise AttributeError except AttributeError as e: raise ValueError('tl_ref_url config value is not set') from e if base[-1] != '/': base += '/' set_classes(options) node = nodes.reference(rawtext, utils.unescape(name), refuri='{}?q={}'.format(base, name), **options) return node # noinspection PyUnusedLocal def tl_role(name, rawtext, text, lineno, inliner, options=None, content=None): """ Link to the TL reference. Returns 2 part tuple containing list of nodes to insert into the document and a list of system messages. Both are allowed to be empty. :param name: The role name used in the document. :param rawtext: The entire markup snippet, with role. :param text: The text marked with the role. :param lineno: The line number where rawtext appears in the input. :param inliner: The inliner instance that called us. :param options: Directive options for customization. :param content: The directive content for customization. """ if options is None: options = {} # TODO Report error on type not found? # Usage: # msg = inliner.reporter.error(..., line=lineno) # return [inliner.problematic(rawtext, rawtext, msg)], [msg] app = inliner.document.settings.env.app node = make_link_node(rawtext, app, text, options) return [node], [] def setup(app): """ Install the plugin. :param app: Sphinx application context. """ app.add_role('tl', tl_role) app.add_config_value('tl_ref_url', None, 'env') return Telethon-1.39.0/readthedocs/developing/000077500000000000000000000000001475566265000200275ustar00rootroot00000000000000Telethon-1.39.0/readthedocs/developing/coding-style.rst000066400000000000000000000015761475566265000231730ustar00rootroot00000000000000============ Coding Style ============ Basically, make it **readable**, while keeping the style similar to the code of whatever file you're working on. Also note that not everyone has 4K screens for their primary monitors, so please try to stick to the 80-columns limit. This makes it easy to ``git diff`` changes from a terminal before committing changes. If the line has to be long, please don't exceed 120 characters. For the commit messages, please make them *explanatory*. Not only they're helpful to troubleshoot when certain issues could have been introduced, but they're also used to construct the change log once a new version is ready. If you don't know enough Python, I strongly recommend reading `Dive Into Python 3 `__, available online for free. For instance, remember to do ``if x is None`` or ``if x is not None`` instead ``if x == None``! Telethon-1.39.0/readthedocs/developing/philosophy.rst000066400000000000000000000021201475566265000227520ustar00rootroot00000000000000========== Philosophy ========== The intention of the library is to have an existing MTProto library existing with hardly any dependencies (indeed, wherever Python is available, you can run this library). Being written in Python means that performance will be nowhere close to other implementations written in, for instance, Java, C++, Rust, or pretty much any other compiled language. However, the library turns out to actually be pretty decent for common operations such as sending messages, receiving updates, or other scripting. Uploading files may be notably slower, but if you would like to contribute, pull requests are appreciated! If ``libssl`` is available on your system, the library will make use of it to speed up some critical parts such as encrypting and decrypting the messages. Files will notably be sent and downloaded faster. The main focus is to keep everything clean and simple, for everyone to understand how working with MTProto and Telegram works. Don't be afraid to read the source, the code won't bite you! It may prove useful when using the library on your own use cases. Telethon-1.39.0/readthedocs/developing/project-structure.rst000066400000000000000000000042521475566265000242700ustar00rootroot00000000000000================= Project Structure ================= Main interface ============== The library itself is under the ``telethon/`` directory. The ``__init__.py`` file there exposes the main ``TelegramClient``, a class that servers as a nice interface with the most commonly used methods on Telegram such as sending messages, retrieving the message history, handling updates, etc. The ``TelegramClient`` inherits from several mixing ``Method`` classes, since there are so many methods that having them in a single file would make maintenance painful (it was three thousand lines before this separation happened!). It's a "god object", but there is only a way to interact with Telegram really. The ``TelegramBaseClient`` is an ABC which will support all of these mixins so they can work together nicely. It doesn't even know how to invoke things because they need to be resolved with user information first (to work with input entities comfortably). The client makes use of the ``network/mtprotosender.py``. The ``MTProtoSender`` is responsible for connecting, reconnecting, packing, unpacking, sending and receiving items from the network. Basically, the low-level communication with Telegram, and handling MTProto-related functions and types such as ``BadSalt``. The sender makes use of a ``Connection`` class which knows the format in which outgoing messages should be sent (how to encode their length and their body, if they're further encrypted). Auto-generated code =================== The files under ``telethon_generator/`` are used to generate the code that gets placed under ``telethon/tl/``. The parsers take in files in a specific format (such as ``.tl`` for objects and ``.json`` for errors) and spit out the generated classes which represent, as Python classes, the request and types defined in the ``.tl`` file. It also constructs an index so that they can be imported easily. Custom documentation can also be generated to easily navigate through the vast amount of items offered by the API. If you clone the repository, you will have to run ``python setup.py gen`` in order to generate the code. Installing the library runs the generator too, but the mentioned command will just generate code. Telethon-1.39.0/readthedocs/developing/telegram-api-in-other-languages.rst000066400000000000000000000012071475566265000266170ustar00rootroot00000000000000=============================== Telegram API in Other Languages =============================== Telethon was made for **Python**, and it has inspired other libraries such as `gramjs `__ (JavaScript) and `grammers `__ (Rust). But there is a lot more beyond those, made independently by different developers. If you're looking for something like Telethon but in a different programming language, head over to `Telegram API in Other Languages in the official wiki `__ for a (mostly) up-to-date list. Telethon-1.39.0/readthedocs/developing/test-servers.rst000066400000000000000000000026361475566265000232360ustar00rootroot00000000000000============ Test Servers ============ To run Telethon on a test server, use the following code: .. code-block:: python client = TelegramClient(None, api_id, api_hash) client.session.set_dc(dc_id, '149.154.167.40', 80) You can check your ``'test ip'`` on https://my.telegram.org. You should set `None` session so to ensure you're generating a new authorization key for it (it would fail if you used a session where you had previously connected to another data center). Note that port 443 might not work, so you can try with 80 instead. Once you're connected, you'll likely be asked to either sign in or sign up. Remember `anyone can access the phone you choose `__, so don't store sensitive data here. Valid phone numbers are ``99966XYYYY``, where ``X`` is the ``dc_id`` and ``YYYY`` is any number you want, for example, ``1234`` in ``dc_id = 2`` would be ``9996621234``. The code sent by Telegram will be ``dc_id`` repeated five times, in this case, ``22222`` so we can hardcode that: .. code-block:: python client = TelegramClient(None, api_id, api_hash) client.session.set_dc(2, '149.154.167.40', 80) client.start( phone='9996621234', code_callback=lambda: '22222' ) Note that Telegram has changed the length of login codes multiple times in the past, so if ``dc_id`` repeated five times does not work, try repeating it six times. Telethon-1.39.0/readthedocs/developing/testing.rst000066400000000000000000000074251475566265000222460ustar00rootroot00000000000000===== Tests ===== Telethon uses `Pytest `__, for testing, `Tox `__ for environment setup, and `pytest-asyncio `__ and `pytest-cov `__ for asyncio and `coverage `__ integration. While reading the full documentation for these is probably a good idea, there is a lot to read, so a brief summary of these tools is provided below for convienience. Brief Introduction to Pytest ============================ `Pytest `__ is a tool for discovering and running python tests, as well as allowing modular reuse of test setup code using fixtures. Most Pytest tests will look something like this:: from module import my_thing, my_other_thing def test_my_thing(fixture): assert my_thing(fixture) == 42 @pytest.mark.asyncio async def test_my_thing(event_loop): assert await my_other_thing(loop=event_loop) == 42 Note here: 1. The test imports one specific function. The role of unit tests is to test that the implementation of some unit, like a function or class, works. It's role is not so much to test that components interact well with each other. I/O, such as connecting to remote servers, should be avoided. This helps with quickly identifying the source of an error, finding silent breakage, and makes it easier to cover all possible code paths. System or integration tests can also be useful, but are currently out of scope of Telethon's automated testing. 2. A function ``test_my_thing`` is declared. Pytest searches for files starting with ``test_``, classes starting with ``Test`` and executes any functions or methods starting with ``test_`` it finds. 3. The function is declared with a parameter ``fixture``. Fixtures are used to request things required to run the test, such as temporary directories, free TCP ports, Connections, etc. Fixtures are declared by simply adding the fixture name as parameter. A full list of available fixtures can be found with the ``pytest --fixtures`` command. 4. The test uses a simple ``assert`` to test some condition is valid. Pytest uses some magic to ensure that the errors from this are readable and easy to debug. 5. The ``pytest.mark.asyncio`` fixture is provided by ``pytest-asyncio``. It starts a loop and executes a test function as coroutine. This should be used for testing asyncio code. It also declares the ``event_loop`` fixture, which will request an ``asyncio`` event loop. Brief Introduction to Tox ========================= `Tox `__ is a tool for automated setup of virtual environments for testing. While the tests can be run directly by just running ``pytest``, this only tests one specific python version in your existing environment, which will not catch e.g. undeclared dependencies, or version incompatabilities. Tox environments are declared in the ``tox.ini`` file. The default environments, declared at the top, can be simply run with ``tox``. The option ``tox -e py36,flake`` can be used to request specific environments to be run. Brief Introduction to Pytest-cov ================================ Coverage is a useful metric for testing. It measures the lines of code and branches that are exercised by the tests. The higher the coverage, the more likely it is that any coding errors will be caught by the tests. A brief coverage report can be generated with the ``--cov`` option to ``tox``, which will be passed on to ``pytest``. Additionally, the very useful HTML report can be generated with ``--cov --cov-report=html``, which contains a browsable copy of the source code, annotated with coverage information for each line. Telethon-1.39.0/readthedocs/developing/tips-for-porting-the-project.rst000066400000000000000000000012711475566265000262270ustar00rootroot00000000000000============================ Tips for Porting the Project ============================ If you're going to use the code on this repository to guide you, please be kind and don't forget to mention it helped you! You should start by reading the source code on the `first release `__ of the project, and start creating a ``MTProtoSender``. Once this is made, you should write by hand the code to authenticate on the Telegram's server, which are some steps required to get the key required to talk to them. Save it somewhere! Then, simply mimic, or reinvent other parts of the code, and it will be ready to go within a few days. Good luck! Telethon-1.39.0/readthedocs/developing/understanding-the-type-language.rst000066400000000000000000000026051475566265000267470ustar00rootroot00000000000000=============================== Understanding the Type Language =============================== `Telegram's Type Language `__ (also known as TL, found on ``.tl`` files) is a concise way to define what other programming languages commonly call classes or structs. Every definition is written as follows for a Telegram object is defined as follows: ``name#id argument_name:argument_type = CommonType`` This means that in a single line you know what the ``TLObject`` name is. You know it's unique ID, and you know what arguments it has. It really isn't that hard to write a generator for generating code to any platform! The generated code should also be able to *encode* the ``TLObject`` (let this be a request or a type) into bytes, so they can be sent over the network. This isn't a big deal either, because you know how the ``TLObject``\ 's are made, and how the types should be serialized. You can either write your own code generator, or use the one this library provides, but please be kind and keep some special mention to this project for helping you out. This is only a introduction. The ``TL`` language is not *that* easy. But it's not that hard either. You're free to sniff the ``telethon_generator/`` files and learn how to parse other more complex lines, such as ``flags`` (to indicate things that may or may not be written at all) and ``vector``\ 's. Telethon-1.39.0/readthedocs/examples/000077500000000000000000000000001475566265000175115ustar00rootroot00000000000000Telethon-1.39.0/readthedocs/examples/chats-and-channels.rst000066400000000000000000000077021475566265000237040ustar00rootroot00000000000000=============================== Working with Chats and Channels =============================== .. note:: These examples assume you have read :ref:`full-api`. .. contents:: Joining a chat or channel ========================= Note that :tl:`Chat` are normal groups, and :tl:`Channel` are a special form of :tl:`Chat`, 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 :tl:`JoinChannelRequest` to join such channel: .. code-block:: python from telethon.tl.functions.channels import JoinChannelRequest await client(JoinChannelRequest(channel)) # In the same way, you can also leave such channel from telethon.tl.functions.channels import LeaveChannelRequest await client(LeaveChannelRequest(input_channel)) For more on channels, check the `channels namespace`__. __ https://tl.telethon.dev/methods/channels/index.html 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 :tl:`ImportChatInviteRequest` as follows: .. code-block:: python from telethon.tl.functions.messages import ImportChatInviteRequest updates = await 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 :tl:`AddChatUserRequest`, which use is very straightforward, or :tl:`InviteToChannelRequest` for channels: .. code-block:: python # For normal chats from telethon.tl.functions.messages import AddChatUserRequest # Note that ``user_to_add`` is NOT the name of the parameter. # It's the user you want to add (``user_id=user_to_add``). await client(AddChatUserRequest( chat_id, user_to_add, fwd_limit=10 # Allow the user to see the 10 last messages )) # For channels (which includes megagroups) from telethon.tl.functions.channels import InviteToChannelRequest await client(InviteToChannelRequest( channel, [users_to_add] )) Note that this method will only really work for friends or bot accounts. Trying to mass-add users with this approach will not work, and can put both your account and group to risk, possibly being flagged as spam and limited. 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 :tl:`CheckChatInviteRequest`, which takes in the hash of said channel or group. 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 :tl:`GetMessagesViewsRequest`, setting ``increment=True``: .. code-block:: python # Obtain `channel' through dialogs or through client.get_entity() or anyhow. # Obtain `msg_ids' through `.get_messages()` or anyhow. Must be a list. await client(GetMessagesViewsRequest( peer=channel, id=msg_ids, increment=True )) Note that you can only do this **once or twice a day** per account, running this in a loop will obviously not increase the views forever unless you wait a day between each iteration. If you run it any sooner than that, the views simply won't be increased. __ 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 Telethon-1.39.0/readthedocs/examples/users.rst000066400000000000000000000030631475566265000214060ustar00rootroot00000000000000===== Users ===== .. note:: These examples assume you have read :ref:`full-api`. .. contents:: Retrieving full information =========================== If you need to retrieve the bio, biography or about information for a user you should use :tl:`GetFullUser`: .. code-block:: python from telethon.tl.functions.users import GetFullUserRequest full = await client(GetFullUserRequest(user)) # or even full = await client(GetFullUserRequest('username')) bio = full.full_user.about See :tl:`UserFull` to know what other fields you can access. Updating your name and/or bio ============================= The first name, last name and bio (about) can all be changed with the same request. Omitted fields won't change after invoking :tl:`UpdateProfile`: .. code-block:: python from telethon.tl.functions.account import UpdateProfileRequest await client(UpdateProfileRequest( about='This is a test from Telethon' )) Updating your username ====================== You need to use :tl:`account.UpdateUsername`: .. code-block:: python from telethon.tl.functions.account import UpdateUsernameRequest await client(UpdateUsernameRequest('new_username')) Updating your profile photo =========================== The easiest way is to upload a new file and use that as the profile photo through :tl:`UploadProfilePhoto`: .. code-block:: python from telethon.tl.functions.photos import UploadProfilePhotoRequest await client(UploadProfilePhotoRequest( await client.upload_file('/path/to/some/file') )) Telethon-1.39.0/readthedocs/examples/word-of-warning.rst000066400000000000000000000013721475566265000232660ustar00rootroot00000000000000================= A Word of Warning ================= Full API is **not** how you are intended to use the library. You **should** always prefer the :ref:`client-ref`. However, not everything is implemented as a friendly method, so full API is your last resort. If you select a method in :ref:`client-ref`, you will most likely find an example for that method. This is how you are intended to use the library. Full API **will** break between different minor versions of the library, since Telegram changes very often. The friendly methods will be kept compatible between major versions. If you need to see real-world examples, please refer to the `wiki page of projects using Telethon `__. Telethon-1.39.0/readthedocs/examples/working-with-messages.rst000066400000000000000000000007351475566265000245060ustar00rootroot00000000000000===================== Working with messages ===================== .. note:: These examples assume you have read :ref:`full-api`. This section has been `moved to the wiki`_, where it can be easily edited as new features arrive and the API changes. Please refer to the linked page to learn how to send spoilers, custom emoji, stickers, react to messages, and more things. .. _moved to the wiki: https://github.com/LonamiWebs/Telethon/wiki/Sending-more-than-just-messages Telethon-1.39.0/readthedocs/index.rst000066400000000000000000000056251475566265000175440ustar00rootroot00000000000000======================== Telethon's Documentation ======================== .. code-block:: python from telethon.sync import TelegramClient, events with TelegramClient('name', api_id, api_hash) as client: client.send_message('me', 'Hello, myself!') print(client.download_profile_photo('me')) @client.on(events.NewMessage(pattern='(?i).*Hello')) async def handler(event): await event.reply('Hey!') client.run_until_disconnected() * Are you new here? Jump straight into :ref:`installation`! * Looking for the method reference? See :ref:`client-ref`. * Did you upgrade the library? Please read :ref:`changelog`. * Used Telethon before v1.0? See :ref:`compatibility-and-convenience`. * Coming from Bot API or want to create new bots? See :ref:`botapi`. * Need the full API reference? https://tl.telethon.dev/. What is this? ------------- Telegram is a popular messaging application. This library is meant to make it easy for you to write Python programs that can interact with Telegram. Think of it as a wrapper that has already done the heavy job for you, so you can focus on developing an application. How should I use the documentation? ----------------------------------- If you are getting started with the library, you should follow the documentation in order by pressing the "Next" button at the bottom-right of every page. You can also use the menu on the left to quickly skip over sections. .. toctree:: :hidden: :caption: First Steps basic/installation basic/signing-in basic/quick-start basic/updates basic/next-steps .. toctree:: :hidden: :caption: Quick References quick-references/faq quick-references/client-reference quick-references/events-reference quick-references/objects-reference .. toctree:: :hidden: :caption: Concepts concepts/strings concepts/entities concepts/chats-vs-channels concepts/updates concepts/sessions concepts/full-api concepts/errors concepts/botapi-vs-mtproto concepts/asyncio .. toctree:: :hidden: :caption: Full API Examples examples/word-of-warning examples/chats-and-channels examples/users examples/working-with-messages .. toctree:: :hidden: :caption: Developing developing/philosophy.rst developing/test-servers.rst developing/project-structure.rst developing/coding-style.rst developing/testing.rst developing/understanding-the-type-language.rst developing/tips-for-porting-the-project.rst developing/telegram-api-in-other-languages.rst .. toctree:: :hidden: :caption: Miscellaneous misc/changelog misc/wall-of-shame.rst misc/compatibility-and-convenience .. toctree:: :hidden: :caption: Telethon Modules modules/client modules/events modules/custom modules/utils modules/errors modules/sessions modules/network modules/helpers Telethon-1.39.0/readthedocs/make.bat000066400000000000000000000014101475566265000172740ustar00rootroot00000000000000@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 Telethon-1.39.0/readthedocs/misc/000077500000000000000000000000001475566265000166265ustar00rootroot00000000000000Telethon-1.39.0/readthedocs/misc/changelog.rst000066400000000000000000005107561475566265000213250ustar00rootroot00000000000000.. _changelog: =========================== Changelog (Version History) =========================== This page lists all the available versions of the library, in chronological order. You should read this when upgrading the library to know where your code can break, and where it can take advantage of new goodies! .. contents:: List of All Versions New layer (v1.39) ================= +------------------------+ | Scheme layer used: 199 | +------------------------+ `View new and changed raw API methods `__. Additions ~~~~~~~~~ * ``drop_media_captions`` added to ``forward_messages``, and documented together with ``drop_author``. * :tl:`InputMediaDocumentExternal` is now recognized when sending albums. Enhancements ~~~~~~~~~~~~ * ``receive_updates=False`` now covers more cases, however, Telegram is still free to ignore it. * Better type-hints in several methods. * Markdown parsing of inline links should cover more cases. * ``range`` is now considered "list-like" and can be used on e.g. ``ids`` parameters. Bug fixes ~~~~~~~~~ * Session is now saved after setting the DC. * Fixed rare crash in entity cache handling when iterating through dialogs. * Fixed IOError that could occur during automatic resizing of some photos. New layer (v1.38) ================= +------------------------+ | Scheme layer used: 193 | +------------------------+ `View new and changed raw API methods `__. Bug fixes ~~~~~~~~~ * Formatting entities misbehaved with albums. * Sending a Message object with a file did not use the new file. New layer (v1.37) ================= +------------------------+ | Scheme layer used: 188 | +------------------------+ `View new and changed raw API methods `__. Additions ~~~~~~~~~ * Support for CDN downloads should be back. Telethon still prefers no CDN by default. Enhancements ~~~~~~~~~~~~ * ``FloodWaitPremium`` should now be handled like any other floodwaits. Bug fixes ~~~~~~~~~ * Fixed edge-case when using ``get_messages(..., reverse=True)``. * ``ConnectionError`` when using proxies should be raised properly. New layer (v1.36) ================= +------------------------+ | Scheme layer used: 181 | +------------------------+ `View new and changed raw API methods `__. Bug fixes ~~~~~~~~~ * Certain updates, such as :tl:`UpdateBotStopped`, should now be processed reliably. New layer (v1.35) ================= +------------------------+ | Scheme layer used: 178 | +------------------------+ `View new and changed raw API methods `__. Additions ~~~~~~~~~ * ``drop_author`` parameter now exposed in ``forward_messages``. Enhancements ~~~~~~~~~~~~ * "Custom secret support" should work with ``TcpMTProxy``. * Some type hints should now be more accurate. Bug fixes ~~~~~~~~~ * Session path couldn't be a ``pathlib.Path`` or ``None``. * Python versions older than 3.9 should now be supported again. * Readthedocs should hopefully build the v1 documentation again. New layer (v1.34) ================= +------------------------+ | Scheme layer used: 173 | +------------------------+ `View new and changed raw API methods `__. Additions ~~~~~~~~~ * ``reply_to_chat`` and ``reply_to_sender`` are now in ``Message``. This is useful when you lack access to the chat, but Telegram still included some basic information. Bug fixes ~~~~~~~~~ * ``parse_mode`` with a custom instance containing both ``parse`` and ``unparse`` should now work. * Parsing and unparsing message entities should now behave better in certain corner-cases. New layer (v1.33) ================= +------------------------+ | Scheme layer used: 167 | +------------------------+ `View new and changed raw API methods `__. Enhancements ~~~~~~~~~~~~ * ``webbrowser`` is now imported conditionally, to support niche environments. * Library should now retry on the suddenly-common ``TimedOutError``. Bug fixes ~~~~~~~~~ * Sending photos which were automatically resized should work again (included in the v1.32 series). New layer (v1.32) ================= +------------------------+ | Scheme layer used: 166 | +------------------------+ `View new and changed raw API methods `__. This enables you to use custom languages in preformatted blocks using HTML: .. code-block:: html
    from telethon import TelegramClient
  
Note that Telethon v1's markdown is a custom format and won't support language tags. If you want to set a custom language, you have to use HTML or a custom formatter. Dropped imghdr support (v1.31) ============================== +------------------------+ | Scheme layer used: 165 | +------------------------+ This release contains a breaking change in preparation for Python 3.12. If you were sending photos from in-memory ``bytes`` or ``BytesIO`` containing images, you should now use ``BytesIO`` and set the ``.name`` property to a dummy name. This will allow Telethon to detect the correct extension (and file type). .. code-block:: python # before image_data = b'...' client.send_file(chat, image_data) # after from io import BytesIO image_data = BytesIO(b'...') image_data.name = 'a.jpg' # any name, only the extension matters client.send_file(chat, image_data) Bug fixes ~~~~~~~~~ * Code generation wasn't working under PyPy. * Obtaining markdown or HTML from message text could produce unexpected results sometimes. * Other fixes for bugs from the previous version, which were already fixed in patch versions. Breaking Changes ~~~~~~~~~~~~~~~~ * ``imghdr`` is deprecated in newer Python versions, so Telethon no longer uses it. This means there might be some cases where Telethon fails to infer the file extension for buffers containing images. If you were relying on this, add ``.name = 'a.jpg'`` (or other extension) to the ``BytesIO`` buffers you upload. Layer bump and small changes (v1.30) ==================================== +------------------------+ | Scheme layer used: 162 | +------------------------+ Some of the bug fixes were already present in patch versions of ``v1.29``, but the new layer necessitated a minor bump. Enhancements ~~~~~~~~~~~~ * Removed client-side checks for editing messages. This only affects ``Message.edit``, as ``client.edit_message`` already had no checks. * Library should not understand more server-side errors during update handling which should reduce crashes. * Client-side image compression should behave better now. Bug fixes ~~~~~~~~~ * Some updates such as ``UpdateChatParticipant`` were being missed due to the order in which Telegram sent them. The library now more carefully checks for the sequence and pts contained in them to avoid dropping them. * Fixed ``is_inline`` check for :tl:`KeyboardButtonWebView`. * Fixed some issues getting entity from cache by ID. * ``reply_to`` should now work when sending albums. More bug fixing (v1.29) ======================= +------------------------+ | Scheme layer used: 160 | +------------------------+ This layer introduces the necessary raw API methods to work with stories. The library is aiming to be "feature-frozen" for as long as v1 is active, so friendly client methods are not implemented, but example code to use stories can be found in the GitHub wiki of the project. Enhancements ~~~~~~~~~~~~ * Removed client-side checks for methods dealing with chat permissions. In particular, this means you can now ban channels. * Improved some error messages and added new classes for more RPC errors. * The client-side check for valid usernames has been loosened, so that very short premium usernames are no longer considered invalid. Bug fixes ~~~~~~~~~ * Attempting to download a thumbnail from documnets without one would fail, rather than do nothing (since nothing can be downloaded if there is no thumb). * More errors are caught in the update handling loop. * HTML ``.text`` should now "unparse" any message contents correctly. * Fixed some problems related to logging. * ``comment_to`` should now work as expected with albums. * ``asyncio.CancelledError`` should now correctly propagate from the update loop. * Removed some absolute imports in favour of relative imports. * ``UserUpdate.last_seen`` should now behave correctly. * Fixed a rare ``ValueError`` during ``connect`` if the session cache was bad. New Layer and housekeeping (v1.28) ================================== +------------------------+ | Scheme layer used: 155 | +------------------------+ Plenty of stale issues closed, as well as improvements for some others. Additions ~~~~~~~~~ * New ``entity_cache_limit`` parameter in the ``TelegramClient`` constructor. This should help a bit in keeping memory usage in check. Enhancements ~~~~~~~~~~~~ * ``progress_callback`` is now called when dealing with albums. See the documentation on `client.send_file() ` for details. * Update state and entities are now periodically saved, so that the information isn't lost in the case of crash or unexpected script terminations. You should still be calling ``disconnect`` or using the context-manager, though. * The client should no longer unnecessarily call ``get_me`` every time it's started. Bug fixes ~~~~~~~~~ * Messages obtained via raw API could not be used in ``forward_messages``. * ``force_sms`` and ``sign_up`` have been deprecated. See `issue 4050`_ for details. It is no longer possible for third-party applications, such as those made with Telethon, to use those features. * ``events.ChatAction`` should now work in more cases in groups with hidden members. * Errors that occur at the connection level should now be properly propagated, so that you can actually have a chance to handle them. * Update handling should be more resilient. * ``PhoneCodeExpiredError`` will correctly clear the stored hash if it occurs in ``sign_in``. * In patch ``v1.28.2``, :tl:`InputBotInlineMessageID64` can now be used to edit inline messages. .. _issue 4050: https://github.com/LonamiWebs/Telethon/issues/4050 New Layer and some Bug fixes (v1.27) ==================================== +------------------------+ | Scheme layer used: 152 | +------------------------+ Bug fixes ~~~~~~~~~ * When the account is logged-out, the library should now correctly propagate an error through ``run_until_disconnected`` to let you handle it. * The library no longer uses ``asyncio.get_event_loop()`` in newer Python versions, which should get rid of some deprecation warnings. * It could happen that bots would receive messages sent by themselves, very often right after they deleted a message. This should happen far less often now (but might still happen with unlucky timings). * Maximum photo size for automatic image resizing is now larger. * The initial request is now correctly wrapped in ``invokeWithoutUpdates`` when updates are disabled after constructing the client instance. * Using a ``pathlib.Path`` to download contacts and web documents should now work correctly. New Layer and some Bug fixes (v1.26) ==================================== +------------------------+ | Scheme layer used: 149 | +------------------------+ This new layer includes things such as emoji status, more admin log events, forum topics and message reactions, among other things. You can access these using raw API. It also contains a few bug fixes. These were fixed in the v1.25 series: * ``client.edit_admin`` did not work on small group chats. * ``client.get_messages`` could stop early in some channels. * ``client.download_profile_photo`` now should work even if ``User.min``. * ``client.disconnect`` should no longer hang when being called from within an event handlers. * ``client.get_dialogs`` now initializes the update state for channels. * The message sender should not need to be fetched in more cases. * Lowered the severity of some log messages to be less spammy. These are new to v1.26.0: * Layer update. * New documented RPC errors. * Sometimes the first message update to a channel could be missed if said message was read immediately. * ``client.get_dialogs`` would fail when the total count evenly divided the chunk size of 100. * ``client.get_messages`` could get stuck during a global search. * Potentially fixed some issues when sending certain videos. * Update handling should be more resilient. * The client should handle having its auth key destroyed more gracefully. * Fixed some issues when logging certain messages. Bug fixes (v1.25.1) =================== This version should fix some of the problems that came with the revamped update handling. * Some inline URLs were not parsing correctly with markdown. * ``events.Raw`` was handling :tl:`UpdateShort` which it shouldn't do. * ``events.Album`` should now work again. * ``CancelledError`` was being incorrectly logged as a fatal error. * Some fixes to update handling primarly aimed for bot accounts. * Update handling now can deal with more errors without crashing. * Unhandled errors from update handling will now be propagated through ``client.run_until_disconnected``. * Invite links with ``+`` are now recognized. * Added new known RPC errors. * ``telethon.types`` could not be used as a module. * 0-length message entities are now stripped to avoid errors. * ``client.send_message`` was not returning a message with ``reply_to`` in some cases. * ``aggressive`` in ``client.iter_participants`` now does nothing (it did not really work anymore anyway, and this should prevent other errors). * ``client.iter_participants`` was failing in some groups. * Text with HTML URLs could sometimes fail to parse. * Added a hard timeout during disconnect in order to prevent the program from freezing. Please be sure to report issues with update handling if you still encounter some errors! Update handling overhaul (v1.25) ================================ +------------------------+ | Scheme layer used: 144 | +------------------------+ I had plans to release v2 way earlier, but my motivation drained off, so that didn't happen. The reason for another v1 release is that there was a clear need to fix some things regarding update handling (which were present in v2). I did not want to make this release. But with the release date for v2 still being unclear, I find it necessary to release another v1 version. I apologize for the delay (I should've done this a lot sooner but didn't because in my head I would've pushed through and finished v2, but I underestimated how much work that was and I probably experienced burn-out). I still don't intend to make new additions to the v1 series (beyond updating the Telegram layer being used). I still have plans to finish v2 some day. But in the meantime, new features, such as reactions, will have to be used through raw API. This update also backports the update overhaul from v2. If you experience issues with updates, please report them on the GitHub page for the project. However, this new update handling should be more reliable, and ``catch_up`` should actually work properly. Breaking Changes ~~~~~~~~~~~~~~~~ * In order for ``catch_up`` to work (new flag in the ``TelegramClient`` constructor), sessions need to impleemnt the new ``get_update_states``. Third-party session storages won't have this implemented by the time this version released, so ``catch_up`` may not work with those. Rushed release to fix login (v1.24) =================================== +------------------------+ | Scheme layer used: 133 | +------------------------+ This is a rushed release. It contains a layer recent enough to not fail with ``UPDATE_APP_TO_LOGIN``, but still not the latest, to avoid breaking more than necessary. Breaking Changes ~~~~~~~~~~~~~~~~ * The biggest change is user identifiers (and chat identifiers, and others) **now use up to 64 bits**, rather than 32. If you were storing them in some storage with fixed size, you may need to update (such as database tables storing only integers). There have been other changes which I currently don't have the time to document. You can refer to the following link to see them early: https://github.com/LonamiWebs/Telethon/compare/v1.23.0...v1.24.0 New schema and bug fixes (v1.23) ================================ +------------------------+ | Scheme layer used: 130 | +------------------------+ `View new and changed raw API methods `__. Enhancements ~~~~~~~~~~~~ * `client.pin_message() ` can now pin on a single side in PMs. * Iterating participants should now be less expensive floodwait-wise. Bug fixes ~~~~~~~~~ * The QR login URL was being encoded incorrectly. * ``force_document`` was being ignored in inline queries for document. * ``manage_call`` permission was accidentally set to ``True`` by default. New schema and bug fixes (v1.22) ================================ +------------------------+ | Scheme layer used: 129 | +------------------------+ `View new and changed raw API methods `__. Enhancements ~~~~~~~~~~~~ * You can now specify a message in `client.get_stats() `. * Metadata extraction from audio files with ``hachoir`` now recognises "artist". * Get default chat permissions by not supplying a user to `client.get_permissions() `. * You may now use ``thumb`` when editing messages. Bug fixes ~~~~~~~~~ * Fixes regarding bot markup in messages. * Gracefully handle :tl:`ChannelForbidden` in ``get_sender``. And from v1.21.1: * ``file.width`` and ``.height`` was not working correctly in photos. * Raw API was mis-interpreting ``False`` values on boolean flag parameters. New schema and QoL improvements (v1.21) ======================================= +------------------------+ | Scheme layer used: 125 | +------------------------+ `View new and changed raw API methods `__. Not many changes in this release, mostly the layer change. Lately quite a few people have been reporting `TypeNotFoundError`, which occurs when the server **sends types that it shouldn't**. This can happen when Telegram decides to add a new, incomplete layer, and then they change the layer without bumping the layer number (so some constructor IDs no longer match and the error occurs). This layer change `should fix it `__. Additions ~~~~~~~~~ * `Message.click() ` now supports a ``password`` parameter, needed when doing things like changing the owner of a bot via `@BotFather `__. Enhancements ~~~~~~~~~~~~ * ``tgcrypto`` will now be used for encryption when installed. Bug fixes ~~~~~~~~~ * `Message.edit ` wasn't working in your own chat on events other than ``NewMessage``. * `client.delete_dialog() ` was not working on chats. * ``events.UserUpdate`` should now handle channels' typing status. * :tl:`InputNotifyPeer` auto-cast should now work on other ``TLObject``. * For some objects, ``False`` was not correctly serialized. New schema and QoL improvements (v1.20) ======================================= +------------------------+ | Scheme layer used: 124 | +------------------------+ `View new and changed raw API methods `__. A bit late to the party, but Telethon now offers a convenient way to comment on channel posts. It works very similar to ``reply_to``: .. code-block:: python client.send_message(channel, 'Great update!', comment_to=1134) This code will leave a comment to the channel post with ID ``1134`` in ``channel``. In addition, the library now logs warning or error messages to ``stderr`` by default! You no longer should be left wondering "why isn't my event handler working" if you forgot to configure logging. It took so long for this change to arrive because nobody noticed that Telethon was using a ``logging.NullHandler`` when it really shouldn't have. If you want the old behaviour of no messages being logged, you can configure `logging` to ``CRITICAL`` severity: .. code-block:: python import logging logging.basicConfig(level=logging.CRITICAL) This is not considered a breaking change because ``stderr`` should only be used for logging purposes, not to emit information others may consume (use ``stdout`` for that). Additions ~~~~~~~~~ * New ``comment_to`` parameter in `client.send_message() `, and `client.send_file() ` to comment on channel posts. Enhancements ~~~~~~~~~~~~ * ``utils.resolve_invite_link`` handles the newer link format. * Downloading files now retries once on `TimeoutError`, which has been happening recently. It is not guaranteed to work, but it should help. * Sending albums of photo URLs is now supported. * EXIF metadata is respected when automatically resizing photos, so the orientation information should no longer be lost. * Downloading a thumbnail by index should now use the correct size ordering. Bug fixes ~~~~~~~~~ * Fixed a `KeyError` on certain cases with ``Conversation``. * Thumbnails should properly render on more clients. Installing ``hachoir`` may help. * Message search was broken when using a certain combination of parameters. * ``utils.resolve_id`` was misbehaving with some identifiers. * Fix ``TypeNotFoundError`` was not being propagated, causing deadlocks. * Invoking multiple requests at once with ``ordered=True`` was deadlocking. New raw API call methods (v1.19) ================================ +------------------------+ | Scheme layer used: 122 | +------------------------+ Telegram has had group calls for some weeks now. This new version contains the raw API methods needed to initiate and manage these group calls, however, the library will likely **not offer ways to stream audio directly**. Telethon's focus is being an asyncio-based, pure-Python implementation to interact with Telegram's API. Streaming audio is beyond the current scope of the project and would be a big undertaking. However, that doesn't mean calls are not possible with Telethon. If you want to help design a Python library to perform audio calls, which can then be used with Telethon (so you can use Telethon + that new library to perform calls with Telethon), please refer to `@pytgcallschat `__ and join the relevant chat to discuss and help with the implementation! The above message was also `posted in the official Telegram group `__, if you wish to discuss it further. With that out of the way, let's list the additions and bug fixes in this release: Additions ~~~~~~~~~ * New ``has_left`` property for user permissions on `client.get_permissions() `. Enhancements ~~~~~~~~~~~~ * Updated documentation and list of known RPC errors. * The library now treats a lack of ping responses as a network error. * `client.kick_participant() ` now returns the service message about the user being kicked, so you can delete it. Bug fixes ~~~~~~~~~ * When editing inline messages, the text parameter is preferred if provided. * Additional senders are unconditionally disconnected when disconnecting the main client, which should reduce the amount of asyncio warnings. * Automatic reconnection with no retries was failing. * :tl:`PhotoPathSize` is now ignored when determining a download size, since this "size" is not a JPEG thumbnail unlike the rest. * `events.ChatAction ` should misbehave less. New layer and QoL improvements (v1.18) ====================================== +------------------------+ | Scheme layer used: 120 | +------------------------+ Mostly fixes, and added some new things that can be done in this new layer. For proxy users, a pull request was merged that will use the ``python-socks`` library when available for proxy support. This library natively supports `asyncio`, so it should work better than the old ``pysocks``. ``pysocks`` will still be used if the new library is not available, and both will be handled transparently by Telethon so you don't need to worry about it. Additions ~~~~~~~~~ * New `client.set_proxy() ` method which lets you change the proxy without recreating the client. You will need to reconnect for it to take effect, but you won't need to recreate the client. This is also an external contribution. * New method to unpin messages `client.unpin_message() `. Enhancements ~~~~~~~~~~~~ * Empty peers are excluded from the list of dialogs. * If the ``python-socks`` library is installed (new optional requirement), it will be used instead of ``pysocks`` for proxy support. This should fix some issues with proxy timeouts, because the new library natively supports `asyncio`. * `client.send_file() ` will now group any media type, instead of sending non-image documents separatedly. This lets you create music albums, for example. * You can now search messages with a ``from_user`` that's not a user. This is a Telegram feature, we know the name isn't great, but backwards-compatibility has to be kept. Bug fixes ~~~~~~~~~ * Fixes related to conversation timeouts. * Large dates (over year 2038) now wrap around a 32-bit integer, which is the only way we can represent them to Telegram. Even if "wrong", it makes things not crash, and it's the best we can do with 32-bit dates. * The library was accidentally using a deprecated argument in one of its friendly methods, producing a warning. * Improvements to the way marked IDs are parsed. * ``SlowModeWaitError`` floods are no longer cached. * Getting the buttons for a message could fail sometimes. * Getting the display name for "forbidden" chats now works. * Better handling of errors in some internal methods. Channel comments and Anonymous Admins (v1.17) ============================================= +------------------------+ | Scheme layer used: 119 | +------------------------+ New minor version, new layer change! This time is a good one to remind every consumer of Python libraries that **you should always specify fixed versions of your dependencies**! If you're using a ``requirements.txt`` file and you want to stick with the old version (or any version) for the time being, you can `use the following syntax `__: .. code-block:: text telethon~=1.16.0 This will install any version compatible with the written version (so, any in the ``1.16`` series). Patch releases will never break your code (and if they do, it's a bug). You can also use that syntax in ``pip install``. Your code can't know what new versions will look like, so saying it will work with all versions is a lie and will cause issues. The reason to bring this up is that Telegram has changed things again, and with the introduction of anonymous administrators and channel comments, the sender of a message may not be a :tl:`User`! To accomodate for this, the field is now a :tl:`Peer` and not `int`. As a reminder, it's always a good idea to use Telethon's friendly methods and custom properties, which have a higher stability guarantee than accessing raw API fields. Even if you don't update, your code will still need to account for the fact that the sender of a message might be one of the accounts Telegram introduced to preserve backwards compatibility, because this is a server-side change, so it's better to update and not lag behind. As it's mostly just a single person driving the project on their free time, bug-fixes are not backported. This version also updates the format of SQLite sessions (the default), so after upgrading and using an old session, the session will be updated, which means trying to use it back in older versions of the library won't work. For backwards-compatibility sake, the library has introduced the properties `Message.reply_to_msg_id ` and `Message.to_id ` that behave like they did before (Telegram has renamed and changed how these fields work). Breaking Changes ~~~~~~~~~~~~~~~~ * ``Message.from_id`` is now a :tl:`Peer`, not `int`! If you want the marked sender ID (much like old behaviour), replace all uses of ``.from_id`` with ``.sender_id``. This will mostly work, but of course in old and new versions you have to account for the fact that this sender may no longer be a user. * You can no longer assign to `Message.reply_to_msg_id ` and `Message.to_id ` because these are now properties that offer a "view" to the real value from a different field. * Answering inline queries with a ``photo`` or ``document`` will now send the photo or document used in the resulting message by default. Not sending the media was technically a bug, but some people may be relying on this old behaviour. You can use the old behaviour with ``include_media=False``. Additions ~~~~~~~~~ * New ``raise_last_call_error`` parameter in the client constructor to raise the same error produced by the last failing call, rather than a generic `ValueError`. * New ``formatting_entities`` parameter in `client.send_message() `, and `client.send_file() ` to bypass the parse mode and manually specify the formatting entities. * New `client.get_permissions() ` method to query a participant's permissions in a group or channel. This request is slightly expensive in small group chats because it has to fetch the entire chat to check just a user, so use of a cache is advised. * `Message.click() ` now works on normal polls! * New ``local_addr`` parameter in the client constructor to use a specific local network address when connecting to Telegram. * `client.inline_query() ` now lets you specify the chat where the query is being made from, which some bots need to provide certain functionality. * You can now get comments in a channel post with the ``reply_to`` parameter in `client.iter_messages() `. Comments are messages that "reply to" a specific channel message, hence the name (which is consistent with how Telegram's API calls it). Enhancements ~~~~~~~~~~~~ * Updated documentation and list of known errors. * If ``hachoir`` is available, the file metadata can now be extracted from streams and in-memory bytes. * The default parameters used to initialize a connection now match the format of those used by Telegram Desktop. * Specifying 0 retries will no longer cause the library to attempt to reconnect. * The library should now be able to reliably download very large files. * Global search should work more reliably now. * Old usernames are evicted from cache, so getting entities by cached username should now be more reliable. * Slightly less noisy logs. * Stability regarding transport-level errors (transport flood, authorization key not found) should be improved. In particular, you should no longer be getting unnecessarily logged out. * Reconnection should no longer occur if the client gets logged out (for example, another client revokes the session). Bug fixes ~~~~~~~~~ * In some cases, there were issues when using `events.Album ` together with `events.Raw `. * For some channels, one of their channel photos would not show up in `client.iter_profile_photos() `. * In some cases, a request that failed to be sent would be forgotten, causing the original caller to be "locked" forever for a response that would never arrive. Failing requests should now consistently be automatically re-sent. * The library should more reliably handle certain updates with "empty" data. * Sending documents in inline queries should now work fine. * Manually using `client.sign_up ` should now work correctly, instead of claiming "code invalid". Special mention to some of the other changes in the 1.16.x series: * The ``thumb`` for ``download_media`` now supports both `str` and :tl:`VideoSize`. * Thumbnails are sorted, so ``-1`` is always the largest. Bug Fixes (v1.16.1) =================== The last release added support to ``force_file`` on any media, including things that were not possible before like ``.webp`` files. However, the ``force_document`` toggle commonly used for photos was applied "twice" (one told the library to send it as a document, and then to send that document as file), which prevented Telegram for analyzing the images. Long story short, sending files to the stickers bot stopped working, but that's been fixed now, and sending photos as documents include the size attribute again as long as Telegram adds it. Enhancements ~~~~~~~~~~~~ * When trying to `client.start() ` to another account if you were previously logged in, the library will now warn you because this is probably not intended. To avoid the warning, make sure you're logging in to the right account or logout from the other first. * Sending a copy of messages with polls will now work when possible. * The library now automatically retries on inter-dc call errors (which occur when Telegram has internal issues). Bug Fixes ~~~~~~~~~ * The aforementioned issue with ``force_document``. * Square brackets removed from IPv6 addresses. This may fix IPv6 support. Channel Statistics (v1.16) ========================== +------------------------+ | Scheme layer used: 116 | +------------------------+ The newest Telegram update has a new method to also retrieve megagroup statistics, which can now be used with `client.get_stats() `. This way you'll be able to access the raw data about your channel or megagroup statistics. The maximum file size limit has also been increased to 2GB on the server, so you can send even larger files. Breaking Changes ~~~~~~~~~~~~~~~~ * Besides the obvious layer change, the ``loop`` argument **is now ignored**. It has been deprecated since Python 3.8 and will be removed in Python 3.10, and also caused some annoying warning messages when using certain parts of the library. If you were (incorrectly) relying on using a different loop from the one that was set, things may break. Enhancements ~~~~~~~~~~~~ * `client.upload_file() ` now works better when streaming files (anything that has a ``.read()``), instead of reading it all into memory when possible. QR login (v1.15) ================ *Published at 2020/07/04* +------------------------+ | Scheme layer used: 114 | +------------------------+ The library now has a friendly method to perform QR-login, as detailed in https://core.telegram.org/api/qr-login. It won't generate QR images, but it provides a way for you to easily do so with any other library of your choice. Additions ~~~~~~~~~ * New `client.qr_login() `. * `message.click ` now lets you click on buttons requesting phone or location. Enhancements ~~~~~~~~~~~~ * Updated documentation and list of known errors. * `events.Album ` should now handle albums from different data centers more gracefully. * `client.download_file() ` now supports `pathlib.Path` as the destination. Bug fixes ~~~~~~~~~ * No longer crash on updates received prior to logging in. * Server-side changes caused clicking on inline buttons to trigger a different error, which is now handled correctly. Minor quality of life improvements (v1.14) ========================================== *Published at 2020/05/26* +------------------------+ | Scheme layer used: 113 | +------------------------+ Some nice things that were missing, along with the usual bug-fixes. Additions ~~~~~~~~~ * New `Message.dice ` property. * The ``func=`` parameter of events can now be an ``async`` function. Bug fixes ~~~~~~~~~ * Fixed `client.action() ` having an alias wrong. * Fixed incorrect formatting of some errors. * Probably more reliable detection of pin events in small groups. * Fixed send methods on `client.conversation() ` were not honoring cancellation. * Flood waits of zero seconds are handled better. * Getting the pinned message in a chat was failing. * Fixed the return value when forwarding messages if some were missing and also the return value of albums. Enhancements ~~~~~~~~~~~~ * ``.tgs`` files are now recognised as animated stickers. * The service message produced by `Message.pin() ` is now returned. * Sending a file with `client.send_file() ` now works fine when you pass an existing dice media (e.g. sending a message copy). * `client.edit_permissions() ` now has the ``embed_links`` parameter which was missing. Bug Fixes (v1.13) ================= *Published at 2020/04/25* +------------------------+ | Scheme layer used: 112 | +------------------------+ Bug fixes and layer bump. Bug fixes ~~~~~~~~~ * Passing ``None`` as the entity to `client.delete_messages() ` would fail. * When downloading a thumbnail, the name inferred was wrong. Bug Fixes (v1.12) ================= *Published at 2020/04/20* +------------------------+ | Scheme layer used: 111 | +------------------------+ Once again nothing major, but a few bug fixes and primarily the new layer deserves a new minor release. Bug fixes ~~~~~~~~~ These were already included in the ``v1.11.3`` patch: * ``libssl`` check was failing on macOS. * Getting input users would sometimes fail on `events.ChatAction `. These bug fixes are available in this release and beyond: * Avoid another occurrence of `MemoryError`. * Sending large files in albums would fail because it tried to cache them. * The ``thumb`` was being ignored when sending files from :tl:`InputFile`. * Fixed editing inline messages from callback queries in some cases. * Proxy connection is now blocking which should help avoid some errors. Bug Fixes (v1.11) ================= *Published at 2020/02/20* +------------------------+ | Scheme layer used: 110 | +------------------------+ It has been a while since the last release, and a few bug fixes have been made since then. This release includes them and updates the scheme layer. Note that most of the bug-fixes are available in the ``v1.10.10`` patch. Bug fixes ~~~~~~~~~ * Fix ``MemoryError`` when casting certain media. * Fix `client.get_entity() ` on small group chats. * `client.delete_dialog() ` now handles deactivated chats more gracefully. * Sending a message with ``file=`` would ignore some of the parameters. * Errors are now un-pickle-able once again. * Fixed some issues regarding markdown and HTML (un)parsing. The following are also present in ``v1.10.10``: * Fixed some issues with `events.Album `. * Fixed some issues with `client.kick_participant() ` and `client.edit_admin() `. * Fixed sending albums and more within `client.conversation() `. * Fixed some import issues. * And a lot more minor stuff. Enhancements ~~~~~~~~~~~~ * Videos can now be included when sending albums. * Getting updates after reconnect should be more reliable. * Updated documentation and added more examples. * More security checks during the generation of the authorization key. The following are also present in ``v1.10.10``: * URLs like ``t.me/@username`` are now valid. * Auto-sleep now works for slow-mode too. * Improved some error messages. * Some internal improvements and updating. * `client.pin_message() ` now also works with message objects. * Asynchronous file descriptors are now allowed during download and upload. Scheduled Messages (v1.10) ========================== *Published at 2019/09/08* +------------------------+ | Scheme layer used: 105 | +------------------------+ You can now schedule messages to be sent (or edited, or forwarded…) at a later time, which can also work as reminders for yourself when used in your own chat! .. code-block:: python from datetime import timedelta # Remind yourself to walk the dog in 10 minutes (after you play with Telethon's update) await client.send_message('me', 'Walk the dog', schedule=timedelta(minutes=10)) # Remind your friend tomorrow to update Telethon await client.send_message(friend, 'Update Telethon!', schedule=timedelta(days=1)) Additions ~~~~~~~~~ * New `Button.auth ` friendly button you can use to ask users to login to your bot. * Telethon's repository now contains ``*.nix`` expressions that you can use. * New `client.kick_participant() ` method to truly kick (not ban) participants. * New ``schedule`` parameter in `client.send_message() `, `client.edit_message() `, `client.forward_messages() ` and `client.send_file() `. Bug fixes ~~~~~~~~~ * Fix calling ``flush`` on file objects which lack this attribute. * Fix `CallbackQuery ` pattern. * Fix `client.action() ` not returning itself when used in a context manager (so the ``as`` would be `None`). * Fix sending :tl:`InputKeyboardButtonUrlAuth` as inline buttons. * Fix `client.edit_permissions() ` defaults. * Fix `Forward ` had its ``client`` as `None`. * Fix (de)serialization of negative timestamps (caused by the information in some sites with instant view, where the date could be very old). * Fix HTML un-parsing. * Fix ``to/from_id`` in private messages when using multiple clients. * Stop disconnecting from `None` (incorrect logging). * Fix double-read on double-connect. * Fix `client.get_messages() ` when being passed more than 100 IDs. * Fix `Message.document ` for documents coming from web-pages. Enhancements ~~~~~~~~~~~~ * Some documentation improvements, including the TL reference. * Documentation now avoids ``telethon.sync``, which should hopefully be less confusing. * Better error messages for flood wait. * You can now `client.get_drafts() ` for a single entity (which means you can now get a single draft from a single chat). * New-style file IDs now work with Telethon. * The ``progress_callback`` for `client.upload_file() ` can now be an ``async def``. Animated Stickers (v1.9) ======================== *Published at 2019/07/06* +------------------------+ | Scheme layer used: 103 | +------------------------+ With the layer 103, Telethon is now able to send and receive animated stickers! These use the ``'application/x-tgsticker'`` mime-type and for now, you can access its raw data, which is a gzipped JSON. Additions ~~~~~~~~~ * New `events.Album ` to easily receive entire albums! * New `client.edit_admin() ` and `client.edit_permissions() ` methods to more easily manage your groups. * New ``pattern=`` in `CallbackQuery `. * New `conversation.cancel_all() ` method, to cancel all currently-active conversations in a particular chat. * New `telethon.utils.encode_waveform` and `telethon.utils.decode_waveform` methods as implemented by Telegram Desktop, which lets you customize how voice notes will render. * New ``ignore_pinned`` parameter in `client.iter_dialogs() `. * New `Message.mark_read() ` method. * You can now use strike-through in markdown with ``~~text~~``, and the corresponding HTML tags for strike-through, quotes and underlined text. * You can now nest entities, as in ``**__text__**``. Bug fixes ~~~~~~~~~ * Fixed downloading contacts. * Fixed `client.iter_dialogs() ` missing some under certain circumstances. * Fixed incredibly slow imports under some systems due to expensive path resolution when searching for ``libssl``. * Fixed captions when sending albums. * Fixed invalid states in `Conversation `. * Fixes to some methods in utils regarding extensions. * Fixed memory cycle in `Forward ` which let you do things like the following: .. code-block:: python original_fwd = message.forward.original_fwd.original_fwd.original_fwd.original_fwd.original_fwd.original_fwd Hopefully you didn't rely on that in your code. * Fixed `File.ext ` not working on unknown mime-types, despite the file name having the extension. * Fixed ``ids=..., reverse=True`` in `client.iter_messages() `. * Fixed `Draft ` not being aware of the entity. * Added missing re-exports in ``telethon.sync``. Enhancements ~~~~~~~~~~~~ * Improved `conversation.cancel() ` behaviour. Now you can use it from anywhere. * The ``progress_callback`` in `client.download_media() ` now lets you use ``async def``. * Improved documentation and the online method reference at https://tl.telethon.dev. Documentation Overhaul (v1.8) ============================= *Published at 2019/05/30* +------------------------+ | Scheme layer used: 100 | +------------------------+ The documentation has been completely reworked from the ground up, with awesome new quick references such as :ref:`client-ref` to help you quickly find what you need! Raw methods also warn you when a friendly variant is available, so that you don't accidentally make your life harder than it has to be. In addition, all methods in the client now are fully annotated with type hints! More work needs to be done, but this should already help a lot when using Telethon from any IDEs. You may have noticed that the patch versions between ``v1.7.2`` to ``v1.7.7`` have not been documented. This is because patch versions should only contain bug fixes, no new features or breaking changes. This hasn't been the case in the past, but from now on, the library will try to adhere more strictly to the `Semantic Versioning `_ principles. If you ever want to look at those bug fixes, please use the appropriated ``git`` command, such as ``git shortlog v1.7.1...v1.7.4``, but in general, they probably just fixed your issue. With that out of the way, let's look at the full change set: Breaking Changes ~~~~~~~~~~~~~~~~ * The layer changed, so take note if you use the raw API, as it's usual. * The way photos are downloaded changed during the layer update of the previous version, and fixing that bug as a breaking change in itself. `client.download_media() ` now offers a different way to deal with thumbnails. Additions ~~~~~~~~~ * New `Message.file ` property! Now you can trivially access `message.file.id ` to get the file ID of some media, or even ``print(message.file.name)``. * Archiving dialogs with `Dialog.archive() ` or `client.edit_folder() ` is now possible. * New cleaned-up method to stream downloads with `client.iter_download() `, which offers a lot of flexibility, such as arbitrary offsets for efficient seeking. * `Dialog.delete() ` has existed for a while, and now `client.delete_dialog() ` exists too so you can easily leave chats or delete dialogs without fetching all dialogs. * Some people or chats have a lot of profile photos. You can now iterate over all of them with the new `client.iter_profile_photos() ` method. * You can now annoy everyone with the new `Message.pin(notify=True) `! The client has its own variant too, called `client.pin_message() `. Bug fixes ~~~~~~~~~ * Correctly catch and raise all RPC errors. * Downloading stripped photos wouldn't work correctly. * Under some systems, ``libssl`` would fail to load earlier than expected, causing the library to fail when being imported. * `conv.get_response() ` after ID 0 wasn't allowed when it should. * `InlineBuilder ` only worked with local files, but files from anywhere are supported. * Accessing the text property from a raw-API call to fetch :tl:`Message` would fail (any any other property that needed the client). * Database is now upgraded if the version was lower, not different. From now on, this should help with upgrades and downgrades slightly. * Fixed saving ``pts`` and session-related stuff. * Disconnection should not raise any errors. * Invite links of the form ``tg://join?invite=`` now work. * `client.iter_participants(search=...) ` now works on private chats again. * Iterating over messages in reverse with a date as offset wouldn't work. * The conversation would behave weirdly when a timeout occurred. Enhancements ~~~~~~~~~~~~ * ``telethon`` now re-export all the goodies that you commonly need when using the library, so e.g. ``from telethon import Button`` will now work. * ``telethon.sync`` now re-exports everything from ``telethon``, so that you can trivially import from just one place everything that you need. * More attempts at reducing CPU usage after automatically fetching missing entities on events. This isn't a big deal, even if it sounds like one. * Hexadecimal invite links are now supported. You didn't need them, but they will now work. Internal Changes ~~~~~~~~~~~~~~~~ * Deterministic code generation. This is good for ``diff``. * On Python 3.7 and above, we properly close the connection. * A lot of micro-optimization. * Fixes to bugs introduced while making this release. * Custom commands on ``setup.py`` are nicer to use. Fix-up for Photo Downloads (v1.7.1) =================================== *Published at 2019/04/24* Telegram changed the way thumbnails (which includes photos) are downloaded, so you can no longer use a :tl:`PhotoSize` alone to download a particular thumbnail size (this is a **breaking change**). Instead, you will have to specify the new ``thumb`` parameter in `client.download_media() ` to download a particular thumbnail size. This addition enables you to easily download thumbnails from documents, something you couldn't do easily before. Easier Events (v1.7) ==================== *Published at 2019/04/22* +-----------------------+ | Scheme layer used: 98 | +-----------------------+ If you have been using Telethon for a while, you probably know how annoying the "Could not find the input entity for…" error can be. In this new version, the library will try harder to find the input entity for you! That is, instead of doing: .. code-block:: python @client.on(events.NewMessage) async def handler(event): await client.download_profile_photo(await event.get_input_sender()) # ...... needs await, it's a method ^^^^^ ^^ You can now do: .. code-block:: python @client.on(events.NewMessage) async def handler(event): await client.download_profile_photo(event.input_sender) # ...... no await, it's a property! ^ # It's also 12 characters shorter :) And even the following will hopefully work: .. code-block:: python @client.on(events.NewMessage) async def handler(event): await client.download_profile_photo(event.sender_id) A lot of people use IDs thinking this is the right way of doing it. Ideally, you would always use ``input_*``, not ``sender`` or ``sender_id`` (and the same applies to chats). But, with this change, IDs will work just the same as ``input_*`` inside events. **This feature still needs some more testing**, so please do open an issue if you find strange behaviour. Breaking Changes ~~~~~~~~~~~~~~~~ * The layer changed, and a lot of things did too. If you are using raw API, you should be careful with this. In addition, some attributes weren't of type ``datetime`` when they should be, which has been fixed. * Due to the layer change, you can no longer download photos with just their :tl:`PhotoSize`. Version 1.7.1 introduces a new way to download thumbnails to work around this issue. * `client.disconnect() ` is now asynchronous again. This means you need to ``await`` it. You don't need to worry about this if you were using ``with client`` or `client.run_until_disconnected `. This should prevent the "pending task was destroyed" errors. Additions ~~~~~~~~~ * New in-memory cache for input entities. This should mean a lot less of disk look-ups. * New `client.action ` method to easily indicate that you are doing some chat action: .. code-block:: python async with client.action(chat, 'typing'): await asyncio.sleep(2) # type for 2 seconds await client.send_message(chat, 'Hello world! I type slow ^^') You can also easily use this for sending files, playing games, etc. New bugs ~~~~~~~~ * Downloading photos is broken. This is fixed in v1.7.1. Bug fixes ~~~~~~~~~ * Fix sending photos from streams/bytes. * Fix unhandled error when sending requests that were too big. * Fix edits that arrive too early on conversations. * Fix `client.edit_message() ` when trying to edit a file. * Fix method calls on the objects returned by `client.iter_dialogs() `. * Attempt at fixing `client.iter_dialogs() ` missing many dialogs. * ``offset_date`` in `client.iter_messages() ` was being ignored in some cases. This has been worked around. * Fix `callback_query.edit() `. * Fix `CallbackQuery(func=...) ` was being ignored. * Fix `UserUpdate ` not working for "typing" (and uploading file, etc.) status. * Fix library was not expecting ``IOError`` from PySocks. * Fix library was raising a generic ``ConnectionError`` and not the one that actually occurred. * Fix the ``blacklist_chats`` parameter in `MessageRead ` not working as intended. * Fix `client.download_media(contact) `. * Fix mime type when sending ``mp3`` files. * Fix forcibly getting the sender or chat from events would not always return all their information. * Fix sending albums with `client.send_file() ` was not returning the sent messages. * Fix forwarding albums with `client.forward_messages() `. * Some fixes regarding filtering updates from chats. * Attempt at preventing duplicated updates. * Prevent double auto-reconnect. Enhancements ~~~~~~~~~~~~ * Some improvements related to proxy connections. * Several updates and improvements to the documentation, such as optional dependencies now being properly listed. * You can now forward messages from different chats directly with `client.forward_messages `. Tidying up Internals (v1.6) =========================== *Published at 2019/02/27* +-----------------------+ | Scheme layer used: 95 | +-----------------------+ First things first, sorry for updating the layer in the previous patch version. That should only be done between major versions ideally, but due to how Telegram works, it's done between minor versions. However raw API has and will always be considered "unsafe", this meaning that you should always use the convenience client methods instead. These methods don't cover the full API yet, so pull requests are welcome. Breaking Changes ~~~~~~~~~~~~~~~~ * The layer update, of course. This didn't really need a mention here. * You can no longer pass a ``batch_size`` when iterating over messages. No other method exposed this parameter, and it was only meant for testing purposes. Instead, it's now a private constant. * ``client.iter_*`` methods no longer have a ``_total`` parameter which was supposed to be private anyway. Instead, they return a new generator object which has a ``.total`` attribute: .. code-block:: python it = client.iter_messages(chat) for i, message in enumerate(it, start=1): percentage = i / it.total print('{:.2%} {}'.format(percentage, message.text)) Additions ~~~~~~~~~ * You can now pass ``phone`` and ``phone_code_hash`` in `client.sign_up `, although you probably don't need that. * Thanks to the overhaul of all ``client.iter_*`` methods, you can now do: .. code-block:: python for message in reversed(client.iter_messages('me')): print(message.text) Bug fixes ~~~~~~~~~ * Fix `telethon.utils.resolve_bot_file_id`, which wasn't working after the layer update (so you couldn't send some files by bot file IDs). * Fix sending albums as bot file IDs (due to image detection improvements). * Fix `takeout() ` failing when they need to download media from other DCs. * Fix repeatedly calling `conversation.get_response() ` when many messages arrived at once (i.e. when several of them were forwarded). * Fixed connecting with `ConnectionTcpObfuscated `. * Fix `client.get_peer_id('me') `. * Fix warning of "missing sqlite3" when in reality it just had wrong tables. * Fix a strange error when using too many IDs in `client.delete_messages() `. * Fix `client.send_file ` with the result of `client.upload_file `. * When answering inline results, their order was not being preserved. * Fix `events.ChatAction ` detecting user leaves as if they were kicked. Enhancements ~~~~~~~~~~~~ * Cleared up some parts of the documentation. * Improved some auto-casts to make life easier. * Improved image detection. Now you can easily send `bytes` and streams of images as photos, unless you force document. * Sending images as photos that are too large will now be resized before uploading, reducing the time it takes to upload them and also avoiding errors when the image was too large (as long as ``pillow`` is installed). The images will remain unchanged if you send it as a document. * Treat ``errors.RpcMcgetFailError`` as a temporary server error to automatically retry shortly. This works around most issues. Internal changes ~~~~~~~~~~~~~~~~ * New common way to deal with retries (``retry_range``). * Cleaned up the takeout client. * Completely overhauled asynchronous generators. Layer Update (v1.5.5) ===================== *Published at 2019/01/14* +-----------------------+ | Scheme layer used: 93 | +-----------------------+ There isn't an entry for v1.5.4 because it contained only one hot-fix regarding loggers. This update is slightly bigger so it deserves mention. Additions ~~~~~~~~~ * New ``supports_streaming`` parameter in `client.send_file `. Bug fixes ~~~~~~~~~ * Dealing with mimetypes should cause less issues in systems like Windows. * Potentially fix alternative session storages that had issues with dates. Enhancements ~~~~~~~~~~~~ * Saner timeout defaults for conversations. * ``Path``-like files are now supported for thumbnails. * Added new hot-keys to the online documentation at https://tl.telethon.dev/ such as ``/`` to search. Press ``?`` to view them all. Bug Fixes (v1.5.3) ================== *Published at 2019/01/14* Several bug fixes and some quality of life enhancements. Breaking Changes ~~~~~~~~~~~~~~~~ * `message.edit ` now respects the previous message buttons or link preview being hidden. If you want to toggle them you need to explicitly set them. This is generally the desired behaviour, but may cause some bots to have buttons when they shouldn't. Additions ~~~~~~~~~ * You can now "hide_via" when clicking on results from `client.inline_query ` to @bing and @gif. * You can now further configure the logger Telethon uses to suit your needs. Bug fixes ~~~~~~~~~ * Fixes for ReadTheDocs to correctly build the documentation. * Fix :tl:`UserEmpty` not being expected when getting the input variant. * The message object returned when sending a message with buttons wouldn't always contain the :tl:`ReplyMarkup`. * Setting email when configuring 2FA wasn't properly supported. * ``utils.resolve_bot_file_id`` now works again for photos. Enhancements ~~~~~~~~~~~~ * Chat and channel participants can now be used as peers. * Reworked README and examples at https://github.com/LonamiWebs/Telethon/tree/master/telethon_examples Takeout Sessions (v1.5.2) ========================= *Published at 2019/01/05* You can now easily start takeout sessions (also known as data export sessions) through `client.takeout() `. Some of the requests will have lower flood limits when done through the takeout session. Bug fixes ~~~~~~~~~ * The new `AdminLogEvent ` had a bug that made it unusable. * `client.iter_dialogs() ` will now locally check for the offset date, since Telegram ignores it. * Answering inline queries with media no works properly. You can now use the library to create inline bots and send stickers through them! object.to_json() (v1.5.1) ========================= *Published at 2019/01/03* The library already had a way to easily convert the objects the API returned into dictionaries through ``object.to_dict()``, but some of the fields are dates or `bytes` which JSON can't serialize directly. For convenience, a new ``object.to_json()`` has been added which will by default format both of those problematic types into something sensible. Additions ~~~~~~~~~ * New `client.iter_admin_log() ` method. Bug fixes ~~~~~~~~~ * `client.is_connected() ` would be wrong when the initial connection failed. * Fixed ``UnicodeDecodeError`` when accessing the text of messages with malformed offsets in their entities. * Fixed `client.get_input_entity() ` for integer IDs that the client has not seen before. Enhancements ~~~~~~~~~~~~ * You can now configure the reply markup when using `Button ` as a bot. * More properties for `Message ` to make accessing media convenient. * Downloading to ``file=bytes`` will now return a `bytes` object with the downloaded media. Polls with the Latest Layer (v1.5) ================================== *Published at 2018/12/25* +-----------------------+ | Scheme layer used: 91 | +-----------------------+ This version doesn't really bring many new features, but rather focuses on updating the code base to support the latest available Telegram layer, 91. This layer brings polls, and you can create and manage them through Telethon! Breaking Changes ~~~~~~~~~~~~~~~~ * The layer change from 82 to 91 changed a lot of things in the raw API, so be aware that if you rely on raw API calls, you may need to update your code, in particular **if you work with files**. They have a new ``file_reference`` parameter that you must provide. Additions ~~~~~~~~~ * New `client.is_bot() ` method. Bug fixes ~~~~~~~~~ * Markdown and HTML parsing now behave correctly with leading whitespace. * HTTP connection should now work correctly again. * Using ``caption=None`` would raise an error instead of setting no caption. * ``KeyError`` is now handled properly when forwarding messages. * `button.click() ` now works as expected for :tl:`KeyboardButtonGame`. Enhancements ~~~~~~~~~~~~ * Some improvements to the search in the full API and generated examples. * Using entities with ``access_hash = 0`` will now work in more cases. Internal changes ~~~~~~~~~~~~~~~~ * Some changes to the documentation and code generation. * 2FA code was updated to work under the latest layer. Error Descriptions in CSV files (v1.4.3) ======================================== *Published at 2018/12/04* While this may seem like a minor thing, it's a big usability improvement. Anyone who wants to update the documentation for known errors, or whether some methods can be used as a bot, user or both, can now be easily edited. Everyone is encouraged to help document this better! Bug fixes ~~~~~~~~~ * ``TimeoutError`` was not handled during automatic reconnects. * Getting messages by ID using :tl:`InputMessageReplyTo` could fail. * Fixed `message.get_reply_message ` as a bot when a user replied to a different bot. * Accessing some document properties in a `Message ` would fail. Enhancements ~~~~~~~~~~~~ * Accessing `events.ChatAction ` properties such as input users may now work in more cases. Internal changes ~~~~~~~~~~~~~~~~ * Error descriptions and information about methods is now loaded from a CSV file instead of being part of several messy JSON files. Bug Fixes (v1.4.2) ================== *Published at 2018/11/24* This version also includes the v1.4.1 hot-fix, which was a single quick fix and didn't really deserve an entry in the changelog. Bug fixes ~~~~~~~~~ * Authorization key wouldn't be saved correctly, requiring re-login. * Conversations with custom events failed to be cancelled. * Fixed ``telethon.sync`` when using other threads. * Fix markdown/HTML parser from failing with leading/trailing whitespace. * Fix accessing ``chat_action_event.input_user`` property. * Potentially improved handling unexpected disconnections. Enhancements ~~~~~~~~~~~~ * Better default behaviour for `client.send_read_acknowledge `. * Clarified some points in the documentation. * Clearer errors for ``utils.get_peer*``. Connection Overhaul (v1.4) ========================== *Published at 2018/11/03* Yet again, a lot of work has been put into reworking the low level connection classes. This means ``asyncio.open_connection`` is now used correctly and the errors it can produce are handled properly. The separation between packing, encrypting and network is now abstracted away properly, so reasoning about the code is easier, making it more maintainable. As a user, you shouldn't worry about this, other than being aware that quite a few changes were made in the insides of the library and you should report any issues that you encounter with this version if any. Breaking Changes ~~~~~~~~~~~~~~~~ * The threaded version of the library will no longer be maintained, primarily because it never was properly maintained anyway. If you have old code, stick with old versions of the library, such as ``0.19.1.6``. * Timeouts no longer accept ``timedelta``. Simply use seconds. * The ``callback`` parameter from `telethon.tl.custom.button.Button.inline()` was removed, since it had always been a bad idea. Adding the callback there meant a lot of extra work for every message sent, and only registering it after the first message was sent! Instead, use `telethon.events.callbackquery.CallbackQuery`. Additions ~~~~~~~~~ * New `dialog.delete() ` method. * New `conversation.cancel() ` method. * New ``retry_delay`` delay for the client to be used on auto-reconnection. Bug fixes ~~~~~~~~~ * Fixed `Conversation.wait_event() `. * Fixed replying with photos/documents on inline results. * `client.is_user_authorized() ` now works correctly after `client.log_out() `. * `dialog.is_group ` now works for :tl:`ChatForbidden`. * Not using ``async with`` when needed is now a proper error. * `events.CallbackQuery ` with string regex was not working properly. * `client.get_entity('me') ` now works again. * Empty codes when signing in are no longer valid. * Fixed file cache for in-memory sessions. Enhancements ~~~~~~~~~~~~ * Support ``next_offset`` in `inline_query.answer() `. * Support ```` mentions in HTML parse mode. * New auto-casts for :tl:`InputDocument` and :tl:`InputChatPhoto`. * Conversations are now exclusive per-chat by default. * The request that caused a RPC error is now shown in the error message. * New full API examples in the generated documentation. * Fixed some broken links in the documentation. * `client.disconnect() ` is now synchronous, but you can still ``await`` it for consistency or compatibility. Event Templates (v1.3) ====================== *Published at 2018/09/22* If you have worked with Flask templates, you will love this update, since it gives you the same features but even more conveniently: .. code-block:: python # handlers/welcome.py from telethon import events @events.register(events.NewMessage('(?i)hello')) async def handler(event): client = event.client await event.respond('Hi!') await client.send_message('me', 'Sent hello to someone') This will `register ` the ``handler`` callback to handle new message events. Note that you didn't add this to any client yet, and this is the key point: you don't need a client to define handlers! You can add it later: .. code-block:: python # main.py from telethon import TelegramClient import handlers.welcome with TelegramClient(...) as client: # This line adds the handler we defined before for new messages client.add_event_handler(handlers.welcome.handler) client.run_until_disconnected() This should help you to split your big code base into a more modular design. Breaking Changes ~~~~~~~~~~~~~~~~ * ``.sender`` is the ``.chat`` when the message is sent in a broadcast channel. This makes sense, because the sender of the message was the channel itself, but you now must take into consideration that it may be either a :tl:`User` or :tl:`Channel` instead of being `None`. Additions ~~~~~~~~~ * New ``MultiError`` class when invoking many requests at once through ``client([requests])``. * New custom ``func=`` on all events. These will receive the entire event, and a good usage example is ``func=lambda e: e.is_private``. * New ``.web_preview`` field on messages. The ``.photo`` and ``.document`` will also return the media in the web preview if any, for convenience. * Callback queries now have a ``.chat`` in most circumstances. Bug fixes ~~~~~~~~~ * Running code with `python3 -O` would remove critical code from asserts. * Fix some rare ghost disconnections after reconnecting. * Fix strange behavior for `send_message(chat, Message, reply_to=foo) `. * The ``loop=`` argument was being pretty much ignored. * Fix ``MemorySession`` file caching. * The logic for getting entities from their username is now correct. * Fixes for sending stickers from ``.webp`` files in Windows, again. * Fix disconnection without being logged in. * Retrieving media from messages would fail. * Getting some messages by ID on private chats. Enhancements ~~~~~~~~~~~~ * `iter_participants ` will now use its ``search=`` as a symbol set when ``aggressive=True``, so you can do ``client.get_participants(group, aggressive=True, search='абвгдеёжзийклмнопрст')``. * The ``StringSession`` supports custom encoding. * Callbacks for `telethon.client.auth.AuthMethods.start` can be ``async``. Internal changes ~~~~~~~~~~~~~~~~ * Cherry-picked a commit to use ``asyncio.open_connection`` in the lowest level of the library. Do open issues if this causes trouble, but it should otherwise improve performance and reliability. * Building and resolving events overhaul. Conversations, String Sessions and More (v1.2) ============================================== *Published at 2018/08/14* This is a big release! Quite a few things have been added to the library, such as the new `Conversation `. This makes it trivial to get tokens from `@BotFather `_: .. code-block:: python from telethon.tl import types with client.conversation('BotFather') as conv: conv.send_message('/mybots') message = conv.get_response() message.click(0) message = conv.get_edit() message.click(0) message = conv.get_edit() for _, token in message.get_entities_text(types.MessageEntityCode): print(token) In addition to that, you can now easily load and export session files without creating any on-disk file thanks to the ``StringSession``: .. code-block:: python from telethon.sessions import StringSession string = StringSession.save(client.session) Check out :ref:`sessions` for more details. For those who aren't able to install ``cryptg``, the support for ``libssl`` has been added back. While interfacing ``libssl`` is not as fast, the speed when downloading and sending files should really be noticeably faster. While those are the biggest things, there are still more things to be excited about. Additions ~~~~~~~~~ - The mentioned method to start a new `client.conversation `. - Implemented global search through `client.iter_messages ` with `None` entity. - New `client.inline_query ` method to perform inline queries. - Bot-API-style ``file_id`` can now be used to send files and download media. You can also access `telethon.utils.resolve_bot_file_id` and `telethon.utils.pack_bot_file_id` to resolve and create these file IDs yourself. Note that each user has its own ID for each file so you can't use a bot's ``file_id`` with your user, except stickers. - New `telethon.utils.get_peer`, useful when you expect a :tl:`Peer`. Bug fixes ~~~~~~~~~ - UTC timezone for `telethon.events.userupdate.UserUpdate`. - Bug with certain input parameters when iterating messages. - RPC errors without parent requests caused a crash, and better logging. - ``incoming = outgoing = True`` was not working properly. - Getting a message's ID was not working. - File attributes not being inferred for ``open()``'ed files. - Use ``MemorySession`` if ``sqlite3`` is not installed by default. - Self-user would not be saved to the session file after signing in. - `client.catch_up() ` seems to be functional again. Enhancements ~~~~~~~~~~~~ - Updated documentation. - Invite links will now use cache, so using them as entities is cheaper. - You can reuse message buttons to send new messages with those buttons. - ``.to_dict()`` will now work even on invalid ``TLObject``'s. Better Custom Message (v1.1.1) ============================== *Published at 2018/07/23* The `custom.Message ` class has been rewritten in a cleaner way and overall feels less hacky in the library. This should perform better than the previous way in which it was patched. The release is primarily intended to test this big change, but also fixes **Python 3.5.2 compatibility** which was broken due to a trailing comma. Bug fixes ~~~~~~~~~ - Using ``functools.partial`` on event handlers broke updates if they had uncaught exceptions. - A bug under some session files where the sender would export authorization for the same data center, which is unsupported. - Some logical bugs in the custom message class. Bot Friendly (v1.1) =================== *Published at 2018/07/21* Two new event handlers to ease creating normal bots with the library, namely `events.InlineQuery ` and `events.CallbackQuery ` for handling ``@InlineBot queries`` or reacting to a button click. For this second option, there is an even better way: .. code-block:: python from telethon.tl.custom import Button async def callback(event): await event.edit('Thank you!') bot.send_message(chat, 'Hello!', buttons=Button.inline('Click me', callback)) You can directly pass the callback when creating the button. This is fine for small bots but it will add the callback every time you send a message, so you probably should do this instead once you are done testing: .. code-block:: python markup = bot.build_reply_markup(Button.inline('Click me', callback)) bot.send_message(chat, 'Hello!', buttons=markup) And yes, you can create more complex button layouts with lists: .. code-block:: python from telethon import events global phone = '' @bot.on(events.CallbackQuery) async def handler(event): global phone if event.data == b'<': phone = phone[:-1] else: phone += event.data.decode('utf-8') await event.answer('Phone is now {}'.format(phone)) markup = bot.build_reply_markup([ [Button.inline('1'), Button.inline('2'), Button.inline('3')], [Button.inline('4'), Button.inline('5'), Button.inline('6')], [Button.inline('7'), Button.inline('8'), Button.inline('9')], [Button.inline('+'), Button.inline('0'), Button.inline('<')], ]) bot.send_message(chat, 'Enter a phone', buttons=markup) (Yes, there are better ways to do this). Now for the rest of things: Additions ~~~~~~~~~ - New `custom.Button ` class to help you create inline (or normal) reply keyboards. You must sign in as a bot to use the ``buttons=`` parameters. - New events usable if you sign in as a bot: `events.InlineQuery ` and `events.CallbackQuery `. - New ``silent`` parameter when sending messages, usable in broadcast channels. - Documentation now has an entire section dedicate to how to use the client's friendly methods at *(removed broken link)*. Bug fixes ~~~~~~~~~ - Empty ``except`` are no longer used which means sending a keyboard interrupt should now work properly. - The ``pts`` of incoming updates could be `None`. - UTC timezone information is properly set for read ``datetime``. - Some infinite recursion bugs in the custom message class. - :tl:`Updates` was being dispatched to raw handlers when it shouldn't. - Using proxies and HTTPS connection mode may now work properly. - Less flood waits when downloading media from different data centers, and the library will now detect them even before sending requests. Enhancements ~~~~~~~~~~~~ - Interactive sign in now supports signing in with a bot token. - ``timedelta`` is now supported where a date is expected, which means you can e.g. ban someone for ``timedelta(minutes=5)``. - Events are only built once and reused many times, which should save quite a few CPU cycles if you have a lot of the same type. - You can now click inline buttons directly if you know their data. Internal changes ~~~~~~~~~~~~~~~~ - When downloading media, the right sender is directly used without previously triggering migrate errors. - Code reusing for getting the chat and the sender, which easily enables this feature for new types. New HTTP(S) Connection Mode (v1.0.4) ==================================== *Published at 2018/07/09* This release implements the HTTP connection mode to the library, which means certain proxies that only allow HTTP connections should now work properly. You can use it doing the following, like any other mode: .. code-block:: python from telethon import TelegramClient, sync from telethon.network import ConnectionHttp client = TelegramClient(..., connection=ConnectionHttp) with client: client.send_message('me', 'Hi!') Additions ~~~~~~~~~ - ``add_mark=`` is now back on ``utils.get_input_peer`` and also on `client.get_input_entity() `. - New `client.get_peer_id ` convenience for ``utils.get_peer_id(await client.get_input_entity(peer))``. Bug fixes ~~~~~~~~~ - If several `TLMessage` in a `MessageContainer` exceeds 1MB, it will no longer be automatically turned into one. This basically means that e.g. uploading 10 file parts at once will work properly again. - Documentation fixes and some missing ``await``. - Revert named argument for `client.forward_messages ` Enhancements ~~~~~~~~~~~~ - New auto-casts to :tl:`InputNotifyPeer` and ``chat_id``. Internal changes ~~~~~~~~~~~~~~~~ - Outgoing `TLMessage` are now pre-packed so if there's an error when serializing the raw requests, the library will no longer swallow it. This also means re-sending packets doesn't need to re-pack their bytes. Iterate Messages in Reverse (v1.0.3) ==================================== *Published at 2018/07/04* +-----------------------+ | Scheme layer used: 82 | +-----------------------+ Mostly bug fixes, but now there is a new parameter on `client.iter_messages ` to support reversing the order in which messages are returned. Additions ~~~~~~~~~ - The mentioned ``reverse`` parameter when iterating over messages. - A new ``sequential_updates`` parameter when creating the client for updates to be processed sequentially. This is useful when you need to make sure that all updates are processed in order, such as a script that only forwards incoming messages somewhere else. Bug fixes ~~~~~~~~~ - Count was always `None` for `message.button_count `. - Some fixes when disconnecting upon dropping the client. - Support for Python 3.4 in the sync version, and fix media download. - Some issues with events when accessing the input chat or their media. - Hachoir wouldn't automatically close the file after reading its metadata. - Signing in required a named ``code=`` parameter, but usage without a name was really widespread so it has been reverted. Bug Fixes (v1.0.2) ================== *Published at 2018/06/28* Updated some asserts and parallel downloads, as well as some fixes for sync. Bug Fixes (v1.0.1) ================== *Published at 2018/06/27* And as usual, every major release has a few bugs that make the library unusable! This quick update should fix those, namely: Bug fixes ~~~~~~~~~ - `client.start() ` was completely broken due to a last-time change requiring named arguments everywhere. - Since the rewrite, if your system clock was wrong, the connection would get stuck in an infinite "bad message" loop of responses from Telegram. - Accessing the buttons of a custom message wouldn't work in channels, which lead to fix a completely different bug regarding starting bots. - Disconnecting could complain if the magic ``telethon.sync`` was imported. - Successful automatic reconnections now ask Telegram to send updates to us once again as soon as the library is ready to listen for them. Synchronous magic (v1.0) ======================== *Published at 2018/06/27* .. important:: If you come from Telethon pre-1.0 you **really** want to read :ref:`compatibility-and-convenience` to port your scripts to the new version. The library has been around for well over a year. A lot of improvements have been made, a lot of user complaints have been fixed, and a lot of user desires have been implemented. It's time to consider the public API as stable, and remove some of the old methods that were around until now for compatibility reasons. But there's one more surprise! There is a new magic ``telethon.sync`` module to let you use **all** the methods in the :ref:`TelegramClient ` (and the types returned from its functions) in a synchronous way, while using `asyncio` behind the scenes! This means you're now able to do both of the following: .. code-block:: python import asyncio async def main(): await client.send_message('me', 'Hello!') asyncio.run(main()) # ...can be rewritten as: from telethon import sync client.send_message('me', 'Hello!') Both ways can coexist (you need to ``await`` if the loop is running). You can also use the magic ``sync`` module in your own classes, and call ``sync.syncify(cls)`` to convert all their ``async def`` into magic variants. Breaking Changes ~~~~~~~~~~~~~~~~ - ``message.get_fwd_sender`` is now in `message.forward `. - ``client.idle`` is now `client.run_until_disconnected() ` - ``client.add_update_handler`` is now `client.add_event_handler ` - ``client.remove_update_handler`` is now `client.remove_event_handler ` - ``client.list_update_handlers`` is now `client.list_event_handlers ` - ``client.get_message_history`` is now `client.get_messages ` - ``client.send_voice_note`` is now `client.send_file ` with ``is_voice=True``. - ``client.invoke()`` is now ``client(...)``. - ``report_errors`` has been removed since it's currently not used, and ``flood_sleep_threshold`` is now part of the client. - The ``update_workers`` and ``spawn_read_thread`` arguments are gone. Simply remove them from your code when you create the client. - Methods with a lot of arguments can no longer be used without specifying their argument. Instead you need to use named arguments. This improves readability and not needing to learn the order of the arguments, which can also change. Additions ~~~~~~~~~ - `client.send_file ` now accepts external ``http://`` and ``https://`` URLs. - You can use the :ref:`TelegramClient ` inside of ``with`` blocks, which will `client.start() ` and `disconnect() ` the client for you: .. code-block:: python from telethon import TelegramClient, sync with TelegramClient(name, api_id, api_hash) as client: client.send_message('me', 'Hello!') Convenience at its maximum! You can even chain the `.start() ` method since it returns the instance of the client: .. code-block:: python with TelegramClient(name, api_id, api_hash).start(bot_token=token) as bot: bot.send_message(chat, 'Hello!') Bug fixes ~~~~~~~~~ - There were some ``@property async def`` left, and some ``await property``. - "User joined" event was being treated as "User was invited". - SQLite's cursor should not be closed properly after usage. - ``await`` the updates task upon disconnection. - Some bug in Python 3.5.2's `asyncio` causing 100% CPU load if you forgot to call `client.disconnect() `. The method is called for you on object destruction, but you still should disconnect manually or use a ``with`` block. - Some fixes regarding disconnecting on client deletion and properly saving the authorization key. - Passing a class to `message.get_entities_text ` now works properly. - Iterating messages from a specific user in private messages now works. Enhancements ~~~~~~~~~~~~ - Both `client.start() ` and `client.run_until_disconnected() ` can be ran in both a synchronous way (without starting the loop manually) or from an ``async def`` where they need to have an ``await``. Core Rewrite in asyncio (v1.0-rc1) ================================== *Published at 2018/06/24* +-----------------------+ | Scheme layer used: 81 | +-----------------------+ This version is a major overhaul of the library internals. The core has been rewritten, cleaned up and refactored to fix some oddities that have been growing inside the library. This means that the code is easier to understand and reason about, including the code flow such as conditions, exceptions, where to reconnect, how the library should behave, and separating different retry types such as disconnections or call fails, but it also means that **some things will necessarily break** in this version. All requests that touch the network are now methods and need to have their ``await`` (or be ran until their completion). Also, the library finally has the simple logo it deserved: a carefully hand-written ``.svg`` file representing a T following Python's colours. Breaking Changes ~~~~~~~~~~~~~~~~ - If you relied on internals like the ``MtProtoSender`` and the ``TelegramBareClient``, both are gone. They are now `MTProtoSender ` and `TelegramBaseClient ` and they behave differently. - Underscores have been renamed from filenames. This means ``telethon.errors.rpc_error_list`` won't work, but you should have been using `telethon.errors` all this time instead. - `client.connect ` no longer returns `True` on success. Instead, you should ``except`` the possible ``ConnectionError`` and act accordingly. This makes it easier to not ignore the error. - You can no longer set ``retries=n`` when calling a request manually. The limit works differently now, and it's done on a per-client basis. - Accessing `.sender `, `.chat ` and similar may *not* work in events anymore, since previously they could access the network. The new rule is that properties are not allowed to make API calls. You should use `.get_sender() `, `.get_chat() ` instead while using events. You can safely access properties if you get messages through `client.get_messages() ` or other methods in the client. - The above point means ``reply_message`` is now `.get_reply_message() `, and ``fwd_from_entity`` is now `get_fwd_sender() `. Also ``forward`` was gone in the previous version, and you should be using ``fwd_from`` instead. Additions ~~~~~~~~~ - Telegram's Terms Of Service are now accepted when creating a new account. This can possibly help avoid bans. This has no effect for accounts that were created before. - The `method reference `_ now shows which methods can be used if you sign in with a ``bot_token``. - There's a new `client.disconnected ` future which you can wait on. When a disconnection occurs, you will now, instead letting it happen in the background. - More configurable retries parameters, such as auto-reconnection, retries when connecting, and retries when sending a request. - You can filter `events.NewMessage ` by sender ID, and also whether they are forwards or not. - New ``ignore_migrated`` parameter for `client.iter_dialogs `. Bug fixes ~~~~~~~~~ - Several fixes to `telethon.events.newmessage.NewMessage`. - Removed named ``length`` argument in ``to_bytes`` for PyPy. - Raw events failed due to not having ``._set_client``. - `message.get_entities_text ` properly supports filtering, even if there are no message entities. - `message.click ` works better. - The server started sending :tl:`DraftMessageEmpty` which the library didn't handle correctly when getting dialogs. - The "correct" chat is now always returned from returned messages. - ``to_id`` was not validated when retrieving messages by their IDs. - ``'__'`` is no longer considered valid in usernames. - The ``fd`` is removed from the reader upon closing the socket. This should be noticeable in Windows. - :tl:`MessageEmpty` is now handled when searching messages. - Fixed a rare infinite loop bug in `client.iter_dialogs ` for some people. - Fixed ``TypeError`` when there is no `.sender `. Enhancements ~~~~~~~~~~~~ - You can now delete over 100 messages at once with `client.delete_messages `. - Signing in now accounts for ``AuthRestartError`` itself, and also handles ``PasswordHashInvalidError``. - ``__all__`` is now defined, so ``from telethon import *`` imports sane defaults (client, events and utils). This is however discouraged and should be used only in quick scripts. - ``pathlib.Path`` is now supported for downloading and uploading media. - Messages you send to yourself are now considered outgoing, unless they are forwarded. - The documentation has been updated with a brand new `asyncio` crash course to encourage you use it. You can still use the threaded version if you want though. - ``.name`` property is now properly supported when sending and downloading files. - Custom ``parse_mode``, which can now be set per-client, support :tl:`MessageEntityMentionName` so you can return those now. - The session file is saved less often, which could result in a noticeable speed-up when working with a lot of incoming updates. Internal changes ~~~~~~~~~~~~~~~~ - The flow for sending a request is as follows: the ``TelegramClient`` creates a ``MTProtoSender`` with a ``Connection``, and the sender starts send and receive loops. Sending a request means enqueueing it in the sender, which will eventually pack and encrypt it with its ``ConnectionState`` instead of using the entire ``Session`` instance. When the data is packed, it will be sent over the ``Connection`` and ultimately over the ``TcpClient``. - Reconnection occurs at the ``MTProtoSender`` level, and receiving responses follows a similar process, but now ``asyncio.Future`` is used for the results which are no longer part of all ``TLObject``, instead are part of the ``TLMessage`` which simplifies things. - Objects can no longer be ``content_related`` and instead subclass ``TLRequest``, making the separation of concerns easier. - The ``TelegramClient`` has been split into several mixin classes to avoid having a 3,000-lines-long file with all the methods. - More special cases in the ``MTProtoSender`` have been cleaned up, and also some attributes from the ``Session`` which didn't really belong there since they weren't being saved. - The ``telethon_generator/`` can now convert ``.tl`` files into ``.json``, mostly as a proof of concept, but it might be useful for other people. Custom Message class (v0.19.1) ============================== *Published at 2018/06/03* +-----------------------+ | Scheme layer used: 80 | +-----------------------+ This update brings a new `telethon.tl.custom.message.Message` object! All the methods in the `telethon.telegram_client.TelegramClient` that used to return a :tl:`Message` will now return this object instead, which means you can do things like the following: .. code-block:: python msg = client.send_message(chat, 'Hello!') msg.edit('Hello there!') msg.reply('Good day!') print(msg.sender) Refer to its documentation to see all you can do, again, click `telethon.tl.custom.message.Message` to go to its page. Breaking Changes ~~~~~~~~~~~~~~~~ - The `telethon.network.connection.common.Connection` class is now an ABC, and the old ``ConnectionMode`` is now gone. Use a specific connection (like `telethon.network.connection.tcpabridged.ConnectionTcpAbridged`) instead. Additions ~~~~~~~~~ - You can get messages by their ID with `telethon.telegram_client.TelegramClient.get_messages`'s ``ids`` parameter: .. code-block:: python message = client.get_messages(chats, ids=123) # Single message message_list = client.get_messages(chats, ids=[777, 778]) # Multiple - More convenience properties for `telethon.tl.custom.dialog.Dialog`. - New default `telethon.telegram_client.TelegramClient.parse_mode`. - You can edit the media of messages that already have some media. - New dark theme in the online ``tl`` reference, check it out at https://tl.telethon.dev/. Bug fixes ~~~~~~~~~ - Some IDs start with ``1000`` and these would be wrongly treated as channels. - Some short usernames like ``@vote`` were being ignored. - `telethon.telegram_client.TelegramClient.iter_messages`'s ``from_user`` was failing if no filter had been set. - `telethon.telegram_client.TelegramClient.iter_messages`'s ``min_id/max_id`` was being ignored by Telegram. This is now worked around. - `telethon.telegram_client.TelegramClient.catch_up` would fail with empty states. - `telethon.events.newmessage.NewMessage` supports ``incoming=False`` to indicate ``outgoing=True``. Enhancements ~~~~~~~~~~~~ - You can now send multiple requests at once while preserving the order: .. code-block:: python from telethon.tl.functions.messages import SendMessageRequest client([SendMessageRequest(chat, 'Hello 1!'), SendMessageRequest(chat, 'Hello 2!')], ordered=True) Internal changes ~~~~~~~~~~~~~~~~ - ``without rowid`` is not used in SQLite anymore. - Unboxed serialization would fail. - Different default limit for ``iter_messages`` and ``get_messages``. - Some clean-up in the ``telethon_generator/`` package. Catching up on Updates (v0.19) ============================== *Published at 2018/05/07* +-----------------------+ | Scheme layer used: 76 | +-----------------------+ This update prepares the library for catching up with updates with the new `telethon.telegram_client.TelegramClient.catch_up` method. This feature needs more testing, but for now it will let you "catch up" on some old updates that occurred while the library was offline, and brings some new features and bug fixes. Additions ~~~~~~~~~ - Add ``search``, ``filter`` and ``from_user`` parameters to `telethon.telegram_client.TelegramClient.iter_messages`. - `telethon.telegram_client.TelegramClient.download_file` now supports a `None` path to return the file in memory and return its `bytes`. - Events now have a ``.original_update`` field. Bug fixes ~~~~~~~~~ - Fixed a race condition when receiving items from the network. - A disconnection is made when "retries reached 0". This hasn't been tested but it might fix the bug. - ``reply_to`` would not override :tl:`Message` object's reply value. - Add missing caption when sending :tl:`Message` with media. Enhancements ~~~~~~~~~~~~ - Retry automatically on ``RpcCallFailError``. This error happened a lot when iterating over many messages, and retrying often fixes it. - Faster `telethon.telegram_client.TelegramClient.iter_messages` by sleeping only as much as needed. - `telethon.telegram_client.TelegramClient.edit_message` now supports omitting the entity if you pass a :tl:`Message`. - `telethon.events.raw.Raw` can now be filtered by type. Internal changes ~~~~~~~~~~~~~~~~ - The library now distinguishes between MTProto and API schemas. - :tl:`State` is now persisted to the session file. - Connection won't retry forever. - Fixed some errors and cleaned up the generation of code. - Fixed typos and enhanced some documentation in general. - Add auto-cast for :tl:`InputMessage` and :tl:`InputLocation`. Pickle-able objects (v0.18.3) ============================= *Published at 2018/04/15* Now you can use Python's ``pickle`` module to serialize ``RPCError`` and any other ``TLObject`` thanks to **@vegeta1k95**! A fix that was fairly simple, but still might be useful for many people. As a side note, the documentation at https://tl.telethon.dev now lists known ``RPCError`` for all requests, so you know what to expect. This required a major rewrite, but it was well worth it! Breaking changes ~~~~~~~~~~~~~~~~ - `telethon.telegram_client.TelegramClient.forward_messages` now returns a single item instead of a list if the input was also a single item. Additions ~~~~~~~~~ - New `telethon.events.messageread.MessageRead` event, to find out when and who read which messages as soon as it happens. - Now you can access ``.chat_id`` on all events and ``.sender_id`` on some. Bug fixes ~~~~~~~~~ - Possibly fix some bug regarding lost ``GzipPacked`` requests. - The library now uses the "real" layer 75, hopefully. - Fixed ``.entities`` name collision on updates by making it private. - ``AUTH_KEY_DUPLICATED`` is handled automatically on connection. - Markdown parser's offset uses ``match.start()`` to allow custom regex. - Some filter types (as a type) were not supported by `telethon.telegram_client.TelegramClient.iter_participants`. - `telethon.telegram_client.TelegramClient.remove_event_handler` works. - `telethon.telegram_client.TelegramClient.start` works on all terminals. - :tl:`InputPeerSelf` case was missing from `telethon.telegram_client.TelegramClient.get_input_entity`. Enhancements ~~~~~~~~~~~~ - The ``parse_mode`` for messages now accepts a callable. - `telethon.telegram_client.TelegramClient.download_media` accepts web previews. - `telethon.tl.custom.dialog.Dialog` instances can now be casted into :tl:`InputPeer`. - Better logging when reading packages "breaks". - Better and more powerful ``setup.py gen`` command. Internal changes ~~~~~~~~~~~~~~~~ - The library won't call ``.get_dialogs()`` on entity not found. Instead, it will ``raise ValueError()`` so you can properly ``except`` it. - Several new examples and updated documentation. - ``py:obj`` is the default Sphinx's role which simplifies ``.rst`` files. - ``setup.py`` now makes use of ``python_requires``. - Events now live in separate files. - Other minor changes. Several bug fixes (v0.18.2) =========================== *Published at 2018/03/27* Just a few bug fixes before they become too many. Additions ~~~~~~~~~ - Getting an entity by its positive ID should be enough, regardless of their type (whether it's an ``User``, a ``Chat`` or a ``Channel``). Although wrapping them inside a ``Peer`` is still recommended, it's not necessary. - New ``client.edit_2fa`` function to change your Two Factor Authentication settings. - ``.stringify()`` and string representation for custom ``Dialog/Draft``. Bug fixes ~~~~~~~~~ - Some bug regarding ``.get_input_peer``. - ``events.ChatAction`` wasn't picking up all the pins. - ``force_document=True`` was being ignored for albums. - Now you're able to send ``Photo`` and ``Document`` as files. - Wrong access to a member on chat forbidden error for ``.get_participants``. An empty list is returned instead. - ``me/self`` check for ``.get[_input]_entity`` has been moved up so if someone has "me" or "self" as their name they won't be retrieved. Iterator methods (v0.18.1) ========================== *Published at 2018/03/17* All the ``.get_`` methods in the ``TelegramClient`` now have a ``.iter_`` counterpart, so you can do operations while retrieving items from them. For instance, you can ``client.iter_dialogs()`` and ``break`` once you find what you're looking for instead fetching them all at once. Another big thing, you can get entities by just their positive ID. This may cause some collisions (although it's very unlikely), and you can (should) still be explicit about the type you want. However, it's a lot more convenient and less confusing. Breaking changes ~~~~~~~~~~~~~~~~ - The library only offers the default ``SQLiteSession`` again. See :ref:`sessions` for more on how to use a different storage from now on. Additions ~~~~~~~~~ - Events now override ``__str__`` and implement ``.stringify()``, just like every other ``TLObject`` does. - ``events.ChatAction`` now has :meth:`respond`, :meth:`reply` and :meth:`delete` for the message that triggered it. - :meth:`client.iter_participants` (and its :meth:`client.get_participants` counterpart) now expose the ``filter`` argument, and the returned users also expose the ``.participant`` they are. - You can now use :meth:`client.remove_event_handler` and :meth:`client.list_event_handlers` similar how you could with normal updates. - New properties on ``events.NewMessage``, like ``.video_note`` and ``.gif`` to access only specific types of documents. - The ``Draft`` class now exposes ``.text`` and ``.raw_text``, as well as a new :meth:`Draft.send` to send it. Bug fixes ~~~~~~~~~ - ``MessageEdited`` was ignoring ``NewMessage`` constructor arguments. - Fixes for ``Event.delete_messages`` which wouldn't handle ``MessageService``. - Bot API style IDs not working on :meth:`client.get_input_entity`. - :meth:`client.download_media` didn't support ``PhotoSize``. Enhancements ~~~~~~~~~~~~ - Less RPC are made when accessing the ``.sender`` and ``.chat`` of some events (mostly those that occur in a channel). - You can send albums larger than 10 items (they will be sliced for you), as well as mixing normal files with photos. - ``TLObject`` now have Python type hints. Internal changes ~~~~~~~~~~~~~~~~ - Several documentation corrections. - :meth:`client.get_dialogs` is only called once again when an entity is not found to avoid flood waits. Sessions overhaul (v0.18) ========================= *Published at 2018/03/04* +-----------------------+ | Scheme layer used: 75 | +-----------------------+ The ``Session``'s have been revisited thanks to the work of **@tulir** and they now use an `ABC `__ so you can easily implement your own! The default will still be a ``SQLiteSession``, but you might want to use the new ``AlchemySessionContainer`` if you need. Refer to the section of the documentation on :ref:`sessions` for more. Breaking changes ~~~~~~~~~~~~~~~~ - ``events.MessageChanged`` doesn't exist anymore. Use the new ``events.MessageEdited`` and ``events.MessageDeleted`` instead. Additions ~~~~~~~~~ - The mentioned addition of new session types. - You can omit the event type on ``client.add_event_handler`` to use ``Raw``. - You can ``raise StopPropagation`` of events if you added several of them. - ``.get_participants()`` can now get up to 90,000 members from groups with 100,000 if when ``aggressive=True``, "bypassing" Telegram's limit. - You now can access ``NewMessage.Event.pattern_match``. - Multiple captions are now supported when sending albums. - ``client.send_message()`` has an optional ``file=`` parameter, so you can do ``events.reply(file='/path/to/photo.jpg')`` and similar. - Added ``.input_`` versions to ``events.ChatAction``. - You can now access the public ``.client`` property on ``events``. - New ``client.forward_messages``, with its own wrapper on ``events``, called ``event.forward_to(...)``. Bug fixes ~~~~~~~~~ - Silly bug regarding ``client.get_me(input_peer=True)``. - ``client.send_voice_note()`` was missing some parameters. - ``client.send_file()`` plays better with streams now. - Incoming messages from bots weren't working with whitelists. - Markdown's URL regex was not accepting newlines. - Better attempt at joining background update threads. - Use the right peer type when a marked integer ID is provided. Internal changes ~~~~~~~~~~~~~~~~ - Resolving ``events.Raw`` is now a no-op. - Logging calls in the ``TcpClient`` to spot errors. - ``events`` resolution is postponed until you are successfully connected, so you can attach them before starting the client. - When an entity is not found, it is searched in *all* dialogs. This might not always be desirable but it's more comfortable for legitimate uses. - Some non-persisting properties from the ``Session`` have been moved out. Further easing library usage (v0.17.4) ====================================== *Published at 2018/02/24* Some new things and patches that already deserved their own release. Additions ~~~~~~~~~ - New ``pattern`` argument to ``NewMessage`` to easily filter messages. - New ``.get_participants()`` convenience method to get members from chats. - ``.send_message()`` now accepts a ``Message`` as the ``message`` parameter. - You can now ``.get_entity()`` through exact name match instead username. - Raise ``ProxyConnectionError`` instead looping forever so you can ``except`` it on your own code and behave accordingly. Bug fixes ~~~~~~~~~ - ``.parse_username`` would fail with ``www.`` or a trailing slash. - ``events.MessageChanged`` would fail with ``UpdateDeleteMessages``. - You can now send ``b'byte strings'`` directly as files again. - ``.send_file()`` was not respecting the original captions when passing another message (or media) as the file. - Downloading media from a different data center would always log a warning for the first time. Internal changes ~~~~~~~~~~~~~~~~ - Use ``req_pq_multi`` instead ``req_pq`` when generating ``auth_key``. - You can use ``.get_me(input_peer=True)`` if all you need is your self ID. - New addition to the interactive client example to show peer information. - Avoid special casing ``InputPeerSelf`` on some ``NewMessage`` events, so you can always safely rely on ``.sender`` to get the right ID. New small convenience functions (v0.17.3) ========================================= *Published at 2018/02/18* More bug fixes and a few others addition to make events easier to use. Additions ~~~~~~~~~ - Use ``hachoir`` to extract video and audio metadata before upload. - New ``.add_event_handler``, ``.add_update_handler`` now deprecated. Bug fixes ~~~~~~~~~ - ``bot_token`` wouldn't work on ``.start()``, and changes to ``password`` (now it will ask you for it if you don't provide it, as docstring hinted). - ``.edit_message()`` was ignoring the formatting (e.g. markdown). - Added missing case to the ``NewMessage`` event for normal groups. - Accessing the ``.text`` of the ``NewMessage`` event was failing due to a bug with the markdown unparser. Internal changes ~~~~~~~~~~~~~~~~ - ``libssl`` is no longer an optional dependency. Use ``cryptg`` instead, which you can find on https://pypi.org/project/cryptg/. New small convenience functions (v0.17.2) ========================================= *Published at 2018/02/15* Primarily bug fixing and a few welcomed additions. Additions ~~~~~~~~~ - New convenience ``.edit_message()`` method on the ``TelegramClient``. - New ``.edit()`` and ``.delete()`` shorthands on the ``NewMessage`` event. - Default to markdown parsing when sending and editing messages. - Support for inline mentions when sending and editing messages. They work like inline urls (e.g. ``[text](@username)``) and also support the Bot-API style (see `here `__). Bug fixes ~~~~~~~~~ - Periodically send ``GetStateRequest`` automatically to keep the server sending updates even if you're not invoking any request yourself. - HTML parsing was failing due to not handling surrogates properly. - ``.sign_up`` was not accepting ``int`` codes. - Whitelisting more than one chat on ``events`` wasn't working. - Video files are sent as a video by default unless ``force_document``. Internal changes ~~~~~~~~~~~~~~~~ - More ``logging`` calls to help spot some bugs in the future. - Some more logic to retrieve input entities on events. - Clarified a few parts of the documentation. Updates as Events (v0.17.1) =========================== *Published at 2018/02/09* Of course there was more work to be done regarding updates, and it's here! The library comes with a new ``events`` module (which you will often import as ``from telethon import TelegramClient, events``). This are pretty much all the additions that come with this version change, but they are a nice addition. Refer to *(removed broken link)* to get started with events. Trust the Server with Updates (v0.17) ===================================== *Published at 2018/02/03* The library trusts the server with updates again. The library will *not* check for duplicates anymore, and when the server kicks us, it will run ``GetStateRequest`` so the server starts sending updates again (something it wouldn't do unless you invoked something, it seems). But this update also brings a few more changes! Additions ~~~~~~~~~ - ``TLObject``'s override ``__eq__`` and ``__ne__``, so you can compare them. - Added some missing cases on ``.get_input_entity()`` and peer functions. - ``obj.to_dict()`` now has a ``'_'`` key with the type used. - ``.start()`` can also sign up now. - More parameters for ``.get_message_history()``. - Updated list of RPC errors. - HTML parsing thanks to **@tulir**! It can be used similar to markdown: ``client.send_message(..., parse_mode='html')``. Enhancements ~~~~~~~~~~~~ - ``client.send_file()`` now accepts ``Message``'s and ``MessageMedia``'s as the ``file`` parameter. - Some documentation updates and fixed to clarify certain things. - New exact match feature on https://tl.telethon.dev. - Return as early as possible from ``.get_input_entity()`` and similar, to avoid penalizing you for doing this right. Bug fixes ~~~~~~~~~ - ``.download_media()`` wouldn't accept a ``Document`` as parameter. - The SQLite is now closed properly on disconnection. - IPv6 addresses shouldn't use square braces. - Fix regarding ``.log_out()``. - The time offset wasn't being used (so having wrong system time would cause the library not to work at all). New ``.resolve()`` method (v0.16.2) =================================== *Published at 2018/01/19* The ``TLObject``'s (instances returned by the API and ``Request``'s) have now acquired a new ``.resolve()`` method. While this should be used by the library alone (when invoking a request), it means that you can now use ``Peer`` types or even usernames where a ``InputPeer`` is required. The object now has access to the ``client``, so that it can fetch the right type if needed, or access the session database. Furthermore, you can reuse requests that need "autocast" (e.g. you put :tl:`User` but ``InputPeer`` was needed), since ``.resolve()`` is called when invoking. Before, it was only done on object construction. Additions ~~~~~~~~~ - Album support. Just pass a list, tuple or any iterable to ``.send_file()``. Enhancements ~~~~~~~~~~~~ - ``.start()`` asks for your phone only if required. - Better file cache. All files under 10MB, once uploaded, should never be needed to be re-uploaded again, as the sent media is cached to the session. Bug fixes ~~~~~~~~~ - ``setup.py`` now calls ``gen_tl`` when installing the library if needed. Internal changes ~~~~~~~~~~~~~~~~ - The mentioned ``.resolve()`` to perform "autocast", more powerful. - Upload and download methods are no longer part of ``TelegramBareClient``. - Reuse ``.on_response()``, ``.__str__`` and ``.stringify()``. Only override ``.on_response()`` if necessary (small amount of cases). - Reduced "autocast" overhead as much as possible. You shouldn't be penalized if you've provided the right type. MtProto 2.0 (v0.16.1) ===================== *Published at 2018/01/11* +-----------------------+ | Scheme layer used: 74 | +-----------------------+ The library is now using MtProto 2.0! This shouldn't really affect you as an end user, but at least it means the library will be ready by the time MtProto 1.0 is deprecated. Additions ~~~~~~~~~ - New ``.start()`` method, to make the library avoid boilerplate code. - ``.send_file`` accepts a new optional ``thumbnail`` parameter, and returns the ``Message`` with the sent file. Bug fixes ~~~~~~~~~ - The library uses again only a single connection. Less updates are be dropped now, and the performance is even better than using temporary connections. - ``without rowid`` will only be used on the ``*.session`` if supported. - Phone code hash is associated with phone, so you can change your mind when calling ``.sign_in()``. Internal changes ~~~~~~~~~~~~~~~~ - File cache now relies on the hash of the file uploaded instead its path, and is now persistent in the ``*.session`` file. Report any bugs on this! - Clearer error when invoking without being connected. - Markdown parser doesn't work on bytes anymore (which makes it cleaner). Sessions as sqlite databases (v0.16) ==================================== *Published at 2017/12/28* In the beginning, session files used to be pickle. This proved to be bad as soon as one wanted to add more fields. For this reason, they were migrated to use JSON instead. But this proved to be bad as soon as one wanted to save things like entities (usernames, their ID and hash), so now it properly uses `sqlite3 `__, which has been well tested, to save the session files! Calling ``.get_input_entity`` using a ``username`` no longer will need to fetch it first, so it's really 0 calls again. Calling ``.get_entity`` will always fetch the most up to date version. Furthermore, nearly everything has been documented, thus preparing the library for `Read the Docs `__ (although there are a few things missing I'd like to polish first), and the `logging `__ are now better placed. Breaking changes ~~~~~~~~~~~~~~~~ - ``.get_dialogs()`` now returns a **single list** instead a tuple consisting of a **custom class** that should make everything easier to work with. - ``.get_message_history()`` also returns a **single list** instead a tuple, with the ``Message`` instances modified to make them more convenient. Both lists have a ``.total`` attribute so you can still know how many dialogs/messages are in total. Additions ~~~~~~~~~ - The mentioned use of ``sqlite3`` for the session file. - ``.get_entity()`` now supports lists too, and it will make as little API calls as possible if you feed it ``InputPeer`` types. Usernames will always be resolved, since they may have changed. - ``.set_proxy()`` method, to avoid having to create a new ``TelegramClient``. - More ``date`` types supported to represent a date parameter. Bug fixes ~~~~~~~~~ - Empty strings weren't working when they were a flag parameter (e.g., setting no last name). - Fix invalid assertion regarding flag parameters as well. - Avoid joining the background thread on disconnect, as it would be `None` due to a race condition. - Correctly handle `None` dates when downloading media. - ``.download_profile_photo`` was failing for some channels. - ``.download_media`` wasn't handling ``Photo``. Internal changes ~~~~~~~~~~~~~~~~ - ``date`` was being serialized as local date, but that was wrong. - ``date`` was being represented as a ``float`` instead of an ``int``. - ``.tl`` parser wasn't stripping inline comments. - Removed some redundant checks on ``update_state.py``. - Use a `synchronized queue `__ instead a hand crafted version. - Use signed integers consistently (e.g. ``salt``). - Always read the corresponding ``TLObject`` from API responses, except for some special cases still. - A few more ``except`` low level to correctly wrap errors. - More accurate exception types. - ``invokeWithLayer(initConnection(X))`` now wraps every first request after ``.connect()``. As always, report if you have issues with some of the changes! IPv6 support (v0.15.5) ====================== *Published at 2017/11/16* +-----------------------+ | Scheme layer used: 73 | +-----------------------+ It's here, it has come! The library now **supports IPv6**! Just pass ``use_ipv6=True`` when creating a ``TelegramClient``. Note that I could *not* test this feature because my machine doesn't have IPv6 setup. If you know IPv6 works in your machine but the library doesn't, please refer to `#425 `_. Additions ~~~~~~~~~ - IPv6 support. - New method to extract the text surrounded by ``MessageEntity``\ 's, in the ``extensions.markdown`` module. Enhancements ~~~~~~~~~~~~ - Markdown parsing is Done Right. - Reconnection on failed invoke. Should avoid "number of retries reached 0" (#270). - Some missing autocast to ``Input*`` types. - The library uses the ``NullHandler`` for ``logging`` as it should have always done. - ``TcpClient.is_connected()`` is now more reliable. .. bug-fixes-1: Bug fixes ~~~~~~~~~ - Getting an entity using their phone wasn't actually working. - Full entities aren't saved unless they have an ``access_hash``, to avoid some `None` errors. - ``.get_message_history`` was failing when retrieving items that had messages forwarded from a channel. General enhancements (v0.15.4) ============================== *Published at 2017/11/04* +-----------------------+ | Scheme layer used: 72 | +-----------------------+ This update brings a few general enhancements that are enough to deserve a new release, with a new feature: beta **markdown-like parsing** for ``.send_message()``! .. additions-1: Additions ~~~~~~~~~ - ``.send_message()`` supports ``parse_mode='md'`` for **Markdown**! It works in a similar fashion to the official clients (defaults to double underscore/asterisk, like ``**this**``). Please report any issues with emojies or enhancements for the parser! - New ``.idle()`` method so your main thread can do useful job (listen for updates). - Add missing ``.to_dict()``, ``__str__`` and ``.stringify()`` for ``TLMessage`` and ``MessageContainer``. .. bug-fixes-2: Bug fixes ~~~~~~~~~ - The list of known peers could end "corrupted" and have users with ``access_hash=None``, resulting in ``struct`` error for it not being an integer. You shouldn't encounter this issue anymore. - The warning for "added update handler but no workers set" wasn't actually working. - ``.get_input_peer`` was ignoring a case for ``InputPeerSelf``. - There used to be an exception when logging exceptions (whoops) on update handlers. - "Downloading contacts" would produce strange output if they had semicolons (``;``) in their name. - Fix some cyclic imports and installing dependencies from the ``git`` repository. - Code generation was using f-strings, which are only supported on Python ≥3.6. Internal changes ~~~~~~~~~~~~~~~~ - The ``auth_key`` generation has been moved from ``.connect()`` to ``.invoke()``. There were some issues were ``.connect()`` failed and the ``auth_key`` was `None` so this will ensure to have a valid ``auth_key`` when needed, even if ``BrokenAuthKeyError`` is raised. - Support for higher limits on ``.get_history()`` and ``.get_dialogs()``. - Much faster integer factorization when generating the required ``auth_key``. Thanks @delivrance for making me notice this, and for the pull request. Bug fixes with updates (v0.15.3) ================================ *Published at 2017/10/20* Hopefully a very ungrateful bug has been removed. When you used to invoke some request through update handlers, it could potentially enter an infinite loop. This has been mitigated and it's now safe to invoke things again! A lot of updates were being dropped (all those gzipped), and this has been fixed too. More bug fixes include a `correct parsing `__ of certain TLObjects thanks to @stek29, and `some `__ `wrong calls `__ that would cause the library to crash thanks to @andr-04, and the ``ReadThread`` not re-starting if you were already authorized. Internally, the ``.to_bytes()`` function has been replaced with ``__bytes__`` so now you can do ``bytes(tlobject)``. Bug fixes and new small features (v0.15.2) ========================================== *Published at 2017/10/14* This release primarly focuses on a few bug fixes and enhancements. Although more stuff may have broken along the way. Enhancements ~~~~~~~~~~~~ - You will be warned if you call ``.add_update_handler`` with no ``update_workers``. - New customizable threshold value on the session to determine when to automatically sleep on flood waits. See ``client.session.flood_sleep_threshold``. - New ``.get_drafts()`` method with a custom ``Draft`` class by @JosXa. - Join all threads when calling ``.disconnect()``, to assert no dangling thread is left alive. - Larger chunk when downloading files should result in faster downloads. - You can use a callable key for the ``EntityDatabase``, so it can be any filter you need. .. bug-fixes-3: Bug fixes ~~~~~~~~~ - ``.get_input_entity`` was failing for IDs and other cases, also making more requests than it should. - Use ``basename`` instead ``abspath`` when sending a file. You can now also override the attributes. - ``EntityDatabase.__delitem__`` wasn't working. - ``.send_message()`` was failing with channels. - ``.get_dialogs(limit=None)`` should now return all the dialogs correctly. - Temporary fix for abusive duplicated updates. .. enhancements-1: .. internal-changes-1: Internal changes ~~~~~~~~~~~~~~~~ - MsgsAck is now sent in a container rather than its own request. - ``.get_input_photo`` is now used in the generated code. - ``.process_entities`` was being called from more places than only ``__call__``. - ``MtProtoSender`` now relies more on the generated code to read responses. Custom Entity Database (v0.15.1) ================================ *Published at 2017/10/05* The main feature of this release is that Telethon now has a custom database for all the entities you encounter, instead depending on ``@lru_cache`` on the ``.get_entity()`` method. The ``EntityDatabase`` will, by default, **cache** all the users, chats and channels you find in memory for as long as the program is running. The session will, by default, save all key-value pairs of the entity identifiers and their hashes (since Telegram may send an ID that it thinks you already know about, we need to save this information). You can **prevent** the ``EntityDatabase`` from saving users by setting ``client.session.entities.enabled = False``, and prevent the ``Session`` from saving input entities at all by setting ``client.session.save_entities = False``. You can also clear the cache for a certain user through ``client.session.entities.clear_cache(entity=None)``, which will clear all if no entity is given. Additions ~~~~~~~~~ - New method to ``.delete_messages()``. - New ``ChannelPrivateError`` class. Enhancements ~~~~~~~~~~~~ - ``.sign_in`` accepts phones as integers. - Changing the IP to which you connect to is as simple as ``client.session.server_address = 'ip'``, since now the server address is always queried from the session. Bug fixes ~~~~~~~~~ - ``.get_dialogs()`` doesn't fail on Windows anymore, and returns the right amount of dialogs. - ``GeneralProxyError`` should be passed to the main thread again, so that you can handle it. Updates Overhaul Update (v0.15) =============================== *Published at 2017/10/01* After hundreds of lines changed on a major refactor, *it's finally here*. It's the **Updates Overhaul Update**; let's get right into it! Breaking changes ~~~~~~~~~~~~~~~~ - ``.create_new_connection()`` is gone for good. No need to deal with this manually since new connections are now handled on demand by the library itself. Enhancements ~~~~~~~~~~~~ - You can **invoke** requests from **update handlers**. And **any other thread**. A new temporary will be made, so that you can be sending even several requests at the same time! - **Several worker threads** for your updates! By default, `None` will spawn. I recommend you to work with ``update_workers=4`` to get started, these will be polling constantly for updates. - You can also change the number of workers at any given time. - The library can now run **in a single thread** again, if you don't need to spawn any at all. Simply set ``spawn_read_thread=False`` when creating the ``TelegramClient``! - You can specify ``limit=None`` on ``.get_dialogs()`` to get **all** of them[1]. - **Updates are expanded**, so you don't need to check if the update has ``.updates`` or an inner ``.update`` anymore. - All ``InputPeer`` entities are **saved in the session** file, but you can disable this by setting ``save_entities=False``. - New ``.get_input_entity`` method, which makes use of the above feature. You **should use this** when a request needs a ``InputPeer``, rather than the whole entity (although both work). - Assert that either all or None dependent-flag parameters are set before sending the request. - Phone numbers can have dashes, spaces, or parenthesis. They'll be removed before making the request. - You can override the phone and its hash on ``.sign_in()``, if you're creating a new ``TelegramClient`` on two different places. Bug fixes ~~~~~~~~~ - ``.log_out()`` was consuming all retries. It should work just fine now. - The session would fail to load if the ``auth_key`` had been removed manually. - ``Updates.check_error`` was popping wrong side, although it's been completely removed. - ``ServerError``\ 's will be **ignored**, and the request will immediately be retried. - Cross-thread safety when saving the session file. - Some things changed on a matter of when to reconnect, so please report any bugs! .. internal-changes-2: Internal changes ~~~~~~~~~~~~~~~~ - ``TelegramClient`` is now only an abstraction over the ``TelegramBareClient``, which can only do basic things, such as invoking requests, working with files, etc. If you don't need any of the abstractions the ``TelegramClient``, you can now use the ``TelegramBareClient`` in a much more comfortable way. - ``MtProtoSender`` is not thread-safe, but it doesn't need to be since a new connection will be spawned when needed. - New connections used to be cached and then reused. Now only their sessions are saved, as temporary connections are spawned only when needed. - Added more RPC errors to the list. **[1]:** Broken due to a condition which should had been the opposite (sigh), fixed 4 commits ahead on https://github.com/LonamiWebs/Telethon/commit/62ea77cbeac7c42bfac85aa8766a1b5b35e3a76c. -------------- **That's pretty much it**, although there's more work to be done to make the overall experience of working with updates *even better*. Stay tuned! Serialization bug fixes (v0.14.2) ================================= *Published at 2017/09/29* Bug fixes ~~~~~~~~~ - **Important**, related to the serialization. Every object or request that had to serialize a ``True/False`` type was always being serialized as `false`! - Another bug that didn't allow you to leave as `None` flag parameters that needed a list has been fixed. Internal changes ~~~~~~~~~~~~~~~~ - Other internal changes include a somewhat more readable ``.to_bytes()`` function and pre-computing the flag instead using bit shifting. The ``TLObject.constructor_id`` has been renamed to ``TLObject.CONSTRUCTOR_ID``, and ``.subclass_of_id`` is also uppercase now. Farewell, BinaryWriter (v0.14.1) ================================ *Published at 2017/09/28* Version ``v0.14`` had started working on the new ``.to_bytes()`` method to dump the ``BinaryWriter`` and its usage on the ``.on_send()`` when serializing TLObjects, and this release finally removes it. The speed up when serializing things to bytes should now be over twice as fast wherever it's needed. Bug fixes ~~~~~~~~~ - This version is again compatible with Python 3.x versions **below 3.5** (there was a method call that was Python 3.5 and above). Internal changes ~~~~~~~~~~~~~~~~ - Using proper classes (including the generated code) for generating authorization keys and to write out ``TLMessage``\ 's. Several requests at once and upload compression (v0.14) ======================================================= *Published at 2017/09/27* New major release, since I've decided that these two features are big enough: Additions ~~~~~~~~~ - Requests larger than 512 bytes will be **compressed through gzip**, and if the result is smaller, this will be uploaded instead. - You can now send **multiple requests at once**, they're simply ``*var_args`` on the ``.invoke()``. Note that the server doesn't guarantee the order in which they'll be executed! Internally, another important change. The ``.on_send`` function on the ``TLObjects`` is **gone**, and now there's a new ``.to_bytes()``. From my tests, this has always been over twice as fast serializing objects, although more replacements need to be done, so please report any issues. Enhancements ~~~~~~~~~~~~ - Implemented ``.get_input_media`` helper methods. Now you can even use another message as input media! Bug fixes ~~~~~~~~~ - Downloading media from CDNs wasn't working (wrong access to a parameter). - Correct type hinting. - Added a tiny sleep when trying to perform automatic reconnection. - Error reporting is done in the background, and has a shorter timeout. - ``setup.py`` used to fail with wrongly generated code. Quick fix-up (v0.13.6) ====================== *Published at 2017/09/23* Before getting any further, here's a quick fix-up with things that should have been on ``v0.13.5`` but were missed. Specifically, the **timeout when receiving** a request will now work properly. Some other additions are a tiny fix when **handling updates**, which was ignoring some of them, nicer ``__str__`` and ``.stringify()`` methods for the ``TLObject``\ 's, and not stopping the ``ReadThread`` if you try invoking something there (now it simply returns `None`). Attempts at more stability (v0.13.5) ==================================== *Published at 2017/09/23* Yet another update to fix some bugs and increase the stability of the library, or, at least, that was the attempt! This release should really **improve the experience with the background thread** that the library starts to read things from the network as soon as it can, but I can't spot every use case, so please report any bug (and as always, minimal reproducible use cases will help a lot). .. bug-fixes-4: Bug fixes ~~~~~~~~~ - ``setup.py`` was failing on Python < 3.5 due to some imports. - Duplicated updates should now be ignored. - ``.send_message`` would crash in some cases, due to having a typo using the wrong object. - ``"socket is None"`` when calling ``.connect()`` should not happen anymore. - ``BrokenPipeError`` was still being raised due to an incorrect order on the ``try/except`` block. .. enhancements-2: Enhancements ~~~~~~~~~~~~ - **Type hinting** for all the generated ``Request``\ 's and ``TLObjects``! IDEs like PyCharm will benefit from this. - ``ProxyConnectionError`` should properly be passed to the main thread for you to handle. - The background thread will only be started after you're authorized on Telegram (i.e. logged in), and several other attempts at polishing the experience with this thread. - The ``Connection`` instance is only created once now, and reused later. - Calling ``.connect()`` should have a better behavior now (like actually *trying* to connect even if we seemingly were connected already). - ``.reconnect()`` behavior has been changed to also be more consistent by making the assumption that we'll only reconnect if the server has disconnected us, and is now private. .. other-changes-1: Internal changes ~~~~~~~~~~~~~~~~ - ``TLObject.__repr__`` doesn't show the original TL definition anymore, it was a lot of clutter. If you have any complaints open an issue and we can discuss it. - Internally, the ``'+'`` from the phone number is now stripped, since it shouldn't be included. - Spotted a new place where ``BrokenAuthKeyError`` would be raised, and it now is raised there. More bug fixes and enhancements (v0.13.4) ========================================= *Published at 2017/09/18* .. new-stuff-1: Additions ~~~~~~~~~ - ``TelegramClient`` now exposes a ``.is_connected()`` method. - Initial authorization on a new data center will retry up to 5 times by default. - Errors that couldn't be handled on the background thread will be raised on the next call to ``.invoke()`` or ``updates.poll()``. .. bugs-fixed-1: Bug fixes ~~~~~~~~~~ - Now you should be able to sign in even if you have ``process_updates=True`` and no previous session. - Some errors and methods are documented a bit clearer. - ``.send_message()`` could randomly fail, as the returned type was not expected. - ``TimeoutError`` is now ignored, since the request will be retried up to 5 times by default. - "-404" errors (``BrokenAuthKeyError``\ 's) are now detected when first connecting to a new data center. - ``BufferError`` is handled more gracefully, in the same way as ``InvalidCheckSumError``\ 's. - Attempt at fixing some "NoneType has no attribute…" errors (with the ``.sender``). Internal changes ~~~~~~~~~~~~~~~~ - Calling ``GetConfigRequest`` is now made less often. - The ``initial_query`` parameter from ``.connect()`` is gone, as it's not needed anymore. - Renamed ``all_tlobjects.layer`` to ``all_tlobjects.LAYER`` (since it's a constant). - The message from ``BufferError`` is now more useful. Bug fixes and enhancements (v0.13.3) ==================================== *Published at 2017/09/14* .. bugs-fixed-2: Bug fixes ~~~~~~~~~ - **Reconnection** used to fail because it tried invoking things from the ``ReadThread``. - Inferring **random ids** for ``ForwardMessagesRequest`` wasn't working. - Downloading media from **CDNs** failed due to having forgotten to remove a single line. - ``TcpClient.close()`` now has a **``threading.Lock``**, so ``NoneType has no close()`` should not happen. - New **workaround** for ``msg seqno too low/high``. Also, both ``Session.id/seq`` are not saved anymore. .. enhancements-3: Enhancements ~~~~~~~~~~~~ - **Request will be retried** up to 5 times by default rather than failing on the first attempt. - ``InvalidChecksumError``\ 's are now **ignored** by the library. - ``TelegramClient.get_entity()`` is now **public**, and uses the ``@lru_cache()`` decorator. - New method to **``.send_voice_note()``**\ 's. - Methods to send message and media now support a **``reply_to`` parameter**. - ``.send_message()`` now returns the **full message** which was just sent. New way to work with updates (v0.13.2) ====================================== *Published at 2017/09/08* This update brings a new way to work with updates, and it's begging for your **feedback**, or better names or ways to do what you can do now. Please refer to the `wiki/Usage Modes `__ for an in-depth description on how to work with updates now. Notice that you cannot invoke requests from within handlers anymore, only the ``v.0.13.1`` patch allowed you to do so. Bug fixes ~~~~~~~~~ - Periodic pings are back. - The username regex mentioned on ``UsernameInvalidError`` was invalid, but it has now been fixed. - Sending a message to a phone number was failing because the type used for a request had changed on layer 71. - CDN downloads weren't working properly, and now a few patches have been applied to ensure more reliability, although I couldn't personally test this, so again, report any feedback. Invoke other requests from within update callbacks (v0.13.1) ============================================================ *Published at 2017/09/04* .. warning:: This update brings some big changes to the update system, so please read it if you work with them! A silly "bug" which hadn't been spotted has now been fixed. Now you can invoke other requests from within your update callbacks. However **this is not advised**. You should post these updates to some other thread, and let that thread do the job instead. Invoking a request from within a callback will mean that, while this request is being invoked, no other things will be read. Internally, the generated code now resides under a *lot* less files, simply for the sake of avoiding so many unnecessary files. The generated code is not meant to be read by anyone, simply to do its job. Unused attributes have been removed from the ``TLObject`` class too, and ``.sign_up()`` returns the user that just logged in in a similar way to ``.sign_in()`` now. Connection modes (v0.13) ======================== *Published at 2017/09/04* +-----------------------+ | Scheme layer used: 71 | +-----------------------+ The purpose of this release is to denote a big change, now you can connect to Telegram through different `**connection modes** `__. Also, a **second thread** will *always* be started when you connect a ``TelegramClient``, despite whether you'll be handling updates or ignoring them, whose sole purpose is to constantly read from the network. The reason for this change is as simple as *"reading and writing shouldn't be related"*. Even when you're simply ignoring updates, this way, once you send a request you will only need to read the result for the request. Whatever Telegram sent before has already been read and outside the buffer. .. additions-2: Additions ~~~~~~~~~ - The mentioned different connection modes, and a new thread. - You can modify the ``Session`` attributes through the ``TelegramClient`` constructor (using ``**kwargs``). - ``RPCError``\ 's now belong to some request you've made, which makes more sense. - ``get_input_*`` now handles `None` (default) parameters more gracefully (it used to crash). .. enhancements-4: Enhancements ~~~~~~~~~~~~ - The low-level socket doesn't use a handcrafted timeout anymore, which should benefit by avoiding the arbitrary ``sleep(0.1)`` that there used to be. - ``TelegramClient.sign_in`` will call ``.send_code_request`` if no ``code`` was provided. Deprecation ~~~~~~~~~~~ - ``.sign_up`` does *not* take a ``phone`` argument anymore. Change this or you will be using ``phone`` as ``code``, and it will fail! The definition looks like ``def sign_up(self, code, first_name, last_name='')``. - The old ``JsonSession`` finally replaces the original ``Session`` (which used pickle). If you were overriding any of these, you should only worry about overriding ``Session`` now. Added verification for CDN file (v0.12.2) ========================================= *Published at 2017/08/28* Since the Content Distributed Network (CDN) is not handled by Telegram itself, the owners may tamper these files. Telegram sends their sha256 sum for clients to implement this additional verification step, which now the library has. If any CDN has altered the file you're trying to download, ``CdnFileTamperedError`` will be raised to let you know. Besides this. ``TLObject.stringify()`` was showing bytes as lists (now fixed) and RPC errors are reported by default: In an attempt to help everyone who works with the Telegram API, Telethon will by default report all Remote Procedure Call errors to `PWRTelegram `__, a public database anyone can query, made by `Daniil `__. All the information sent is a GET request with the error code, error message and method used. .. note:: If you still would like to opt out, simply set ``client.session.report_errors = False`` to disable this feature. However Daniil would really thank you if you helped him (and everyone) by keeping it on! CDN support (v0.12.1) ===================== *Published at 2017/08/24* The biggest news for this update are that downloading media from CDN's (you'll often encounter this when working with popular channels) now **works**. Bug fixes ~~~~~~~~~ - The method used to download documents crashed because two lines were swapped. - Determining the right path when downloading any file was very weird, now it's been enhanced. - The ``.sign_in()`` method didn't support integer values for the code! Now it does again. Some important internal changes are that the old way to deal with RSA public keys now uses a different module instead the old strange hand-crafted version. Hope the new, super simple ``README.rst`` encourages people to use Telethon and make it better with either suggestions, or pull request. Pull requests are *super* appreciated, but showing some support by leaving a star also feels nice ⭐️. Newbie friendly update (v0.12) ============================== *Published at 2017/08/22* +-----------------------+ | Scheme layer used: 70 | +-----------------------+ This update is overall an attempt to make Telethon a bit more user friendly, along with some other stability enhancements, although it brings quite a few changes. Breaking changes ~~~~~~~~~~~~~~~~ - The ``TelegramClient`` methods ``.send_photo_file()``, ``.send_document_file()`` and ``.send_media_file()`` are now a **single method** called ``.send_file()``. It's also important to note that the **order** of the parameters has been **swapped**: first to *who* you want to send it, then the file itself. - The same applies to ``.download_msg_media()``, which has been renamed to ``.download_media()``. The method now supports a ``Message`` itself too, rather than only ``Message.media``. The specialized ``.download_photo()``, ``.download_document()`` and ``.download_contact()`` still exist, but are private. Additions ~~~~~~~~~ - Updated to **layer 70**! - Both downloading and uploading now support **stream-like objects**. - A lot **faster initial connection** if ``sympy`` is installed (can be installed through ``pip``). - ``libssl`` will also be used if available on your system (likely on Linux based systems). This speed boost should also apply to uploading and downloading files. - You can use a **phone number** or an **username** for methods like ``.send_message()``, ``.send_file()``, and all the other quick-access methods provided by the ``TelegramClient``. .. bug-fixes-5: Bug fixes ~~~~~~~~~ - Crashing when migrating to a new layer and receiving old updates should not happen now. - ``InputPeerChannel`` is now casted to ``InputChannel`` automtically too. - ``.get_new_msg_id()`` should now be thread-safe. No promises. - Logging out on macOS caused a crash, which should be gone now. - More checks to ensure that the connection is flagged correctly as either connected or not. .. note:: Downloading files from CDN's will **not work** yet (something new that comes with layer 70). -------------- That's it, any new idea or suggestion about how to make the project even more friendly is highly appreciated. .. note:: Did you know that you can pretty print any result Telegram returns (called ``TLObject``\ 's) by using their ``.stringify()`` function? Great for debugging! get_input_* now works with vectors (v0.11.5) ============================================= *Published at 2017/07/11* Quick fix-up of a bug which hadn't been encountered until now. Auto-cast by using ``get_input_*`` now works. get_input_* everywhere (v0.11.4) ================================= *Published at 2017/07/10* For some reason, Telegram doesn't have enough with the `InputPeer `__. There also exist `InputChannel `__ and `InputUser `__! You don't have to worry about those anymore, it's handled internally now. Besides this, every Telegram object now features a new default ``.__str__`` look, and also a `.stringify() method `__ to pretty format them, if you ever need to inspect them. The library now uses `the DEBUG level `__ everywhere, so no more warnings or information messages if you had logging enabled. The ``no_webpage`` parameter from ``.send_message`` `has been renamed `__ to ``link_preview`` for clarity, so now it does the opposite (but has a clearer intention). Quick .send_message() fix (v0.11.3) =================================== *Published at 2017/07/05* A very quick follow-up release to fix a tiny bug with ``.send_message()``, no new features. Callable TelegramClient (v0.11.2) ================================= *Published at 2017/07/04* +-----------------------+ | Scheme layer used: 68 | +-----------------------+ There is a new preferred way to **invoke requests**, which you're encouraged to use: .. code:: python # New! result = client(SomeRequest()) # Old. result = client.invoke(SomeRequest()) Existing code will continue working, since the old ``.invoke()`` has not been deprecated. When you ``.create_new_connection()``, it will also handle ``FileMigrateError``\ 's for you, so you don't need to worry about those anymore. .. bugs-fixed-3: Bugs fixes ~~~~~~~~~~ - Fixed some errors when installing Telethon via ``pip`` (for those using either source distributions or a Python version ≤ 3.5). - ``ConnectionResetError`` didn't flag sockets as closed, but now it does. On a more technical side, ``msg_id``\ 's are now more accurate. Improvements to the updates (v0.11.1) ===================================== *Published at 2017/06/24* Receiving new updates shouldn't miss any anymore, also, periodic pings are back again so it should work on the long run. On a different order of things, ``.connect()`` also features a timeout. Notice that the ``timeout=`` is **not** passed as a **parameter** anymore, and is instead specified when creating the ``TelegramClient``. Bug fixes ~~~~~~~~~ - Fixed some name class when a request had a ``.msg_id`` parameter. - The correct amount of random bytes is now used in DH request - Fixed ``CONNECTION_APP_VERSION_EMPTY`` when using temporary sessions. - Avoid connecting if already connected. Support for parallel connections (v0.11) ======================================== *Published at 2017/06/16* *This update brings a lot of changes, so it would be nice if you could* **read the whole change log**! Breaking changes ~~~~~~~~~~~~~~~~ - Every Telegram error has now its **own class**, so it's easier to fine-tune your ``except``\ 's. - Markdown parsing is **not part** of Telethon itself anymore, although there are plans to support it again through a some external module. - The ``.list_sessions()`` has been moved to the ``Session`` class instead. - The ``InteractiveTelegramClient`` is **not** shipped with ``pip`` anymore. Additions ~~~~~~~~~ - A new, more **lightweight class** has been added. The ``TelegramBareClient`` is now the base of the normal ``TelegramClient``, and has the most basic features. - New method to ``.create_new_connection()``, which can be ran **in parallel** with the original connection. This will return the previously mentioned ``TelegramBareClient`` already connected. - Any file object can now be used to download a file (for instance, a ``BytesIO()`` instead a file name). - Vales like ``random_id`` are now **automatically inferred**, so you can save yourself from the hassle of writing ``generate_random_long()`` everywhere. Same applies to ``.get_input_peer()``, unless you really need the extra performance provided by skipping one ``if`` if called manually. - Every type now features a new ``.to_dict()`` method. .. bug-fixes-6: Bug fixes ~~~~~~~~~ - Received errors are acknowledged to the server, so they don't happen over and over. - Downloading media on different data centers is now up to **x2 faster**, since there used to be an ``InvalidDCError`` for each file part tried to be downloaded. - Lost messages are now properly skipped. - New way to handle the **result of requests**. The old ``ValueError`` "*The previously sent request must be resent. However, no request was previously sent (possibly called from a different thread).*" *should* not happen anymore. Internal changes ~~~~~~~~~~~~~~~~ - Some fixes to the ``JsonSession``. - Fixed possibly crashes if trying to ``.invoke()`` a ``Request`` while ``.reconnect()`` was being called on the ``UpdatesThread``. - Some improvements on the ``TcpClient``, such as not switching between blocking and non-blocking sockets. - The code now uses ASCII characters only. - Some enhancements to ``.find_user_or_chat()`` and ``.get_input_peer()``. JSON session file (v0.10.1) =========================== *Published at 2017/06/07* This version is primarily for people to **migrate** their ``.session`` files, which are *pickled*, to the new *JSON* format. Although slightly slower, and a bit more vulnerable since it's plain text, it's a lot more resistant to upgrades. .. warning:: You **must** upgrade to this version before any higher one if you've used Telethon ≤ v0.10. If you happen to upgrade to an higher version, that's okay, but you will have to manually delete the ``*.session`` file, and logout from that session from an official client. Additions ~~~~~~~~~ - New ``.get_me()`` function to get the **current** user. - ``.is_user_authorized()`` is now more reliable. - New nice button to copy the ``from telethon.tl.xxx.yyy import Yyy`` on the online documentation. - **More error codes** added to the ``errors`` file. Enhancements ~~~~~~~~~~~~ - Everything on the documentation is now, theoretically, **sorted alphabetically**. - No second thread is spawned unless one or more update handlers are added. Full support for different DCs and ++stable (v0.10) =================================================== *Published at 2017/06/03* Working with **different data centers** finally *works*! On a different order of things, **reconnection** is now performed automatically every time Telegram decides to kick us off their servers, so now Telethon can really run **forever and ever**! In theory. Enhancements ~~~~~~~~~~~~ - **Documentation** improvements, such as showing the return type. - The ``msg_id too low/high`` error should happen **less often**, if any. - Sleeping on the main thread is **not done anymore**. You will have to ``except FloodWaitError``\ 's. - You can now specify your *own application version*, device model, system version and language code. - Code is now more *pythonic* (such as making some members private), and other internal improvements (which affect the **updates thread**), such as using ``logger`` instead a bare ``print()`` too. This brings Telethon a whole step closer to ``v1.0``, though more things should preferably be changed. Stability improvements (v0.9.1) =============================== *Published at 2017/05/23* Telethon used to crash a lot when logging in for the very first time. The reason for this was that the reconnection (or dead connections) were not handled properly. Now they are, so you should be able to login directly, without needing to delete the ``*.session`` file anymore. Notice that downloading from a different DC is still a WIP. Enhancements ~~~~~~~~~~~~ - Updates thread is only started after a successful login. - Files meant to be ran by the user now use **shebangs** and proper permissions. - In-code documentation now shows the returning type. - **Relative import** is now used everywhere, so you can rename ``telethon`` to anything else. - **Dead connections** are now **detected** instead entering an infinite loop. - **Sockets** can now be **closed** (and re-opened) properly. - Telegram decided to update the layer 66 without increasing the number. This has been fixed and now we're up-to-date again. General improvements (v0.9) =========================== *Published at 2017/05/19* +-----------------------+ | Scheme layer used: 66 | +-----------------------+ Additions ~~~~~~~~~ - The **documentation**, available online `here `__, has a new search bar. - Better **cross-thread safety** by using ``threading.Event``. - More improvements for running Telethon during a **long period of time**. Bug fixes ~~~~~~~~~ - **Avoid a certain crash on login** (occurred if an unexpected object ID was received). - Avoid crashing with certain invalid UTF-8 strings. - Avoid crashing on certain terminals by using known ASCII characters where possible. - The ``UpdatesThread`` is now a daemon, and should cause less issues. - Temporary sessions didn't actually work (with ``session=None``). Internal changes ~~~~~~~~~~~~~~~~ - ``.get_dialogs(count=`` was renamed to ``.get_dialogs(limit=``. Bot login and proxy support (v0.8) ================================== *Published at 2017/04/14* Additions ~~~~~~~~~ - **Bot login**, thanks to @JuanPotato for hinting me about how to do it. - **Proxy support**, thanks to @exzhawk for implementing it. - **Logging support**, used by passing ``--telethon-log=DEBUG`` (or ``INFO``) as a command line argument. Bug fixes ~~~~~~~~~ - Connection fixes, such as avoiding connection until ``.connect()`` is explicitly invoked. - Uploading big files now works correctly. - Fix uploading big files. - Some fixes on the updates thread, such as correctly sleeping when required. Long-run bug fix (v0.7.1) ========================= *Published at 2017/02/19* If you're one of those who runs Telethon for a long time (more than 30 minutes), this update by @strayge will be great for you. It sends periodic pings to the Telegram servers so you don't get disconnected and you can still send and receive updates! Two factor authentication (v0.7) ================================ *Published at 2017/01/31* +-----------------------+ | Scheme layer used: 62 | +-----------------------+ If you're one of those who love security the most, these are good news. You can now use two factor authentication with Telethon too! As internal changes, the coding style has been improved, and you can easily use custom session objects, and various little bugs have been fixed. Updated pip version (v0.6) ========================== *Published at 2016/11/13* +-----------------------+ | Scheme layer used: 57 | +-----------------------+ This release has no new major features. However, it contains some small changes that make using Telethon a little bit easier. Now those who have installed Telethon via ``pip`` can also take advantage of changes, such as less bugs, creating empty instances of ``TLObjects``, specifying a timeout and more! Ready, pip, go! (v0.5) ====================== *Published at 2016/09/18* Telethon is now available as a **`Python package `__**! Those are really exciting news (except, sadly, the project structure had to change *a lot* to be able to do that; but hopefully it won't need to change much more, any more!) Not only that, but more improvements have also been made: you're now able to both **sign up** and **logout**, watch a pretty "Uploading/Downloading… x%" progress, and other minor changes which make using Telethon **easier**. Made InteractiveTelegramClient cool (v0.4) ========================================== *Published at 2016/09/12* Yes, really cool! I promise. Even though this is meant to be a *library*, that doesn't mean it can't have a good *interactive client* for you to try the library out. This is why now you can do many, many things with the ``InteractiveTelegramClient``: - **List dialogs** (chats) and pick any you wish. - **Send any message** you like, text, photos or even documents. - **List** the **latest messages** in the chat. - **Download** any message's media (photos, documents or even contacts!). - **Receive message updates** as you talk (i.e., someone sent you a message). It actually is a usable-enough client for your day by day. You could even add ``libnotify`` and pop, you're done! A great cli-client with desktop notifications. Also, being able to download and upload media implies that you can do the same with the library itself. Did I need to mention that? Oh, and now, with even less bugs! I hope. Media revolution and improvements to update handling! (v0.3) ============================================================ *Published at 2016/09/11* Telegram is more than an application to send and receive messages. You can also **send and receive media**. Now, this implementation also gives you the power to upload and download media from any message that contains it! Nothing can now stop you from filling up all your disk space with all the photos! If you want to, of course. Handle updates in their own thread! (v0.2) ========================================== *Published at 2016/09/10* This version handles **updates in a different thread** (if you wish to do so). This means that both the low level ``TcpClient`` and the not-so-low-level ``MtProtoSender`` are now multi-thread safe, so you can use them with more than a single thread without worrying! This also implies that you won't need to send a request to **receive an update** (is someone typing? did they send me a message? has someone gone offline?). They will all be received **instantly**. Some other cool examples of things that you can do: when someone tells you "*Hello*", you can automatically reply with another "*Hello*" without even needing to type it by yourself :) However, be careful with spamming!! Do **not** use the program for that! First working alpha version! (v0.1) =================================== *Published at 2016/09/06* +-----------------------+ | Scheme layer used: 55 | +-----------------------+ There probably are some bugs left, which haven't yet been found. However, the majority of code works and the application is already usable! Not only that, but also uses the latest scheme as of now *and* handles way better the errors. This tag is being used to mark this release as stable enough. Telethon-1.39.0/readthedocs/misc/compatibility-and-convenience.rst000066400000000000000000000141241475566265000252650ustar00rootroot00000000000000.. _compatibility-and-convenience: ============================= Compatibility and Convenience ============================= Telethon is an `asyncio` library. Compatibility is an important concern, and while it can't always be kept and mistakes happens, the :ref:`changelog` is there to tell you when these important changes happen. .. contents:: Compatibility ============= Some decisions when developing will inevitable be proven wrong in the future. One of these decisions was using threads. Now that Python 3.4 is reaching EOL and using `asyncio` is usable as of Python 3.5 it makes sense for a library like Telethon to make a good use of it. If you have old code, **just use old versions** of the library! There is nothing wrong with that other than not getting new updates or fixes, but using a fixed version with ``pip install telethon==0.19.1.6`` is easy enough to do. You might want to consider using `Virtual Environments `_ in your projects. There's no point in maintaining a synchronous version because the whole point is that people don't have time to upgrade, and there has been several changes and clean-ups. Using an older version is the right way to go. Sometimes, other small decisions are made. These all will be reflected in the :ref:`changelog` which you should read when upgrading. If you want to jump the `asyncio` boat, here are some of the things you will need to start migrating really old code: .. code-block:: python # 1. Import the client from telethon.sync from telethon.sync import TelegramClient # 2. Change this monster... try: assert client.connect() if not client.is_user_authorized(): client.send_code_request(phone_number) me = client.sign_in(phone_number, input('Enter code: ')) ... # REST OF YOUR CODE finally: client.disconnect() # ...for this: with client: ... # REST OF YOUR CODE # 3. client.idle() no longer exists. # Change this... client.idle() # ...to this: client.run_until_disconnected() # 4. client.add_update_handler no longer exists. # Change this... client.add_update_handler(handler) # ...to this: client.add_event_handler(handler) In addition, all the update handlers must be ``async def``, and you need to ``await`` method calls that rely on network requests, such as getting the chat or sender. If you don't use updates, you're done! Convenience =========== .. note:: The entire documentation assumes you have done one of the following: .. code-block:: python from telethon import TelegramClient, sync # or from telethon.sync import TelegramClient This makes the examples shorter and easier to think about. For quick scripts that don't need updates, it's a lot more convenient to forget about `asyncio` and just work with sequential code. This can prove to be a powerful hybrid for running under the Python REPL too. .. code-block:: python from telethon.sync import TelegramClient # ^~~~~ note this part; it will manage the asyncio loop for you with TelegramClient(...) as client: print(client.get_me().username) # ^ notice the lack of await, or loop.run_until_complete(). # Since there is no loop running, this is done behind the scenes. # message = client.send_message('me', 'Hi!') import time time.sleep(5) message.delete() # You can also have an hybrid between a synchronous # part and asynchronous event handlers. # from telethon import events @client.on(events.NewMessage(pattern='(?i)hi|hello')) async def handler(event): await event.reply('hey') client.run_until_disconnected() Some methods, such as ``with``, ``start``, ``disconnect`` and ``run_until_disconnected`` work both in synchronous and asynchronous contexts by default for convenience, and to avoid the little overhead it has when using methods like sending a message, getting messages, etc. This keeps the best of both worlds as a sane default. .. note:: As a rule of thumb, if you're inside an ``async def`` and you need the client, you need to ``await`` calls to the API. If you call other functions that also need API calls, make them ``async def`` and ``await`` them too. Otherwise, there is no need to do so with this mode. Speed ===== When you're ready to micro-optimize your application, or if you simply don't need to call any non-basic methods from a synchronous context, just get rid of ``telethon.sync`` and work inside an ``async def``: .. code-block:: python import asyncio from telethon import TelegramClient, events async def main(): async with TelegramClient(...) as client: print((await client.get_me()).username) # ^_____________________^ notice these parenthesis # You want to ``await`` the call, not the username. # message = await client.send_message('me', 'Hi!') await asyncio.sleep(5) await message.delete() @client.on(events.NewMessage(pattern='(?i)hi|hello')) async def handler(event): await event.reply('hey') await client.run_until_disconnected() asyncio.run(main()) The ``telethon.sync`` magic module essentially wraps every method behind: .. code-block:: python asyncio.run(main()) With some other tricks, so that you don't have to write it yourself every time. That's the overhead you pay if you import it, and what you save if you don't. Learning ======== You know the library uses `asyncio` everywhere, and you want to learn how to do things right. Even though `asyncio` is its own topic, the documentation wants you to learn how to use Telethon correctly, and for that, you need to use `asyncio` correctly too. For this reason, there is a section called :ref:`mastering-asyncio` that will introduce you to the `asyncio` world, with links to more resources for learning how to use it. Feel free to check that section out once you have read the rest. Telethon-1.39.0/readthedocs/misc/wall-of-shame.rst000066400000000000000000000046421475566265000220220ustar00rootroot00000000000000============= Wall of Shame ============= This project has an `issues `__ section for you to file **issues** whenever you encounter any when working with the library. Said section is **not** for issues on *your* program but rather issues with Telethon itself. If you have not made the effort to 1. read through the docs and 2. `look for the method you need `__, you will end up on the `Wall of Shame `__, i.e. all issues labeled `"RTFM" `__: **rtfm** Literally "Read The F--king Manual"; a term showing the frustration of being bothered with questions so trivial that the asker could have quickly figured out the answer on their own with minimal effort, usually by reading readily-available documents. People who say"RTFM!" might be considered rude, but the true rude ones are the annoying people who take absolutely no self-responibility and expect to have all the answers handed to them personally. *"Damn, that's the twelveth time that somebody posted this question to the messageboard today! RTFM, already!"* *by Bill M. July 27, 2004* If you have indeed read the docs, and have tried looking for the method, and yet you didn't find what you need, **that's fine**. Telegram's API can have some obscure names at times, and for this reason, there is a `"question" label `__ with questions that are okay to ask. Just state what you've tried so that we know you've made an effort, or you'll go to the Wall of Shame. Of course, if the issue you're going to open is not even a question but a real issue with the library (thankfully, most of the issues have been that!), you won't end up here. Don't worry. Current winner -------------- The current winner is `issue 213 `__: **Issue:** .. figure:: https://user-images.githubusercontent.com/6297805/29822978-9a9a6ef0-8ccd-11e7-9ec5-934ea0f57681.jpg :alt: Winner issue Winner issue **Answer:** .. figure:: https://user-images.githubusercontent.com/6297805/29822983-9d523402-8ccd-11e7-9fb1-5783740ee366.jpg :alt: Winner issue answer Winner issue answer Telethon-1.39.0/readthedocs/modules/000077500000000000000000000000001475566265000173435ustar00rootroot00000000000000Telethon-1.39.0/readthedocs/modules/client.rst000066400000000000000000000044301475566265000213540ustar00rootroot00000000000000.. _telethon-client: ============== TelegramClient ============== .. currentmodule:: telethon.client The `TelegramClient ` aggregates several mixin classes to provide all the common functionality in a nice, Pythonic interface. Each mixin has its own methods, which you all can use. **In short, to create a client you must run:** .. code-block:: python from telethon import TelegramClient client = TelegramClient(name, api_id, api_hash) async def main(): # Now you can use all client methods listed below, like for example... await client.send_message('me', 'Hello to myself!') with client: client.loop.run_until_complete(main()) You **don't** need to import these `AuthMethods`, `MessageMethods`, etc. Together they are the `TelegramClient ` and you can access all of their methods. See :ref:`client-ref` for a short summary. .. automodule:: telethon.client.telegramclient :members: :undoc-members: :show-inheritance: .. automodule:: telethon.client.telegrambaseclient :members: :undoc-members: :show-inheritance: .. automodule:: telethon.client.account :members: :undoc-members: :show-inheritance: .. automodule:: telethon.client.auth :members: :undoc-members: :show-inheritance: .. automodule:: telethon.client.bots :members: :undoc-members: :show-inheritance: .. automodule:: telethon.client.buttons :members: :undoc-members: :show-inheritance: .. automodule:: telethon.client.chats :members: :undoc-members: :show-inheritance: .. automodule:: telethon.client.dialogs :members: :undoc-members: :show-inheritance: .. automodule:: telethon.client.downloads :members: :undoc-members: :show-inheritance: .. automodule:: telethon.client.messageparse :members: :undoc-members: :show-inheritance: .. automodule:: telethon.client.messages :members: :undoc-members: :show-inheritance: .. automodule:: telethon.client.updates :members: :undoc-members: :show-inheritance: .. automodule:: telethon.client.uploads :members: :undoc-members: :show-inheritance: .. automodule:: telethon.client.users :members: :undoc-members: :show-inheritance: Telethon-1.39.0/readthedocs/modules/custom.rst000066400000000000000000000051101475566265000214040ustar00rootroot00000000000000============== Custom package ============== The `telethon.tl.custom` package contains custom classes that the library uses in order to make working with Telegram easier. Only those that you are supposed to use will be documented here. You can use undocumented ones at your own risk. More often than not, you don't need to import these (unless you want type hinting), nor do you need to manually create instances of these classes. They are returned by client methods. .. contents:: .. automodule:: telethon.tl.custom :members: :undoc-members: :show-inheritance: AdminLogEvent ============= .. automodule:: telethon.tl.custom.adminlogevent :members: :undoc-members: :show-inheritance: Button ====== .. automodule:: telethon.tl.custom.button :members: :undoc-members: :show-inheritance: ChatGetter ========== .. automodule:: telethon.tl.custom.chatgetter :members: :undoc-members: :show-inheritance: Conversation ============ .. automodule:: telethon.tl.custom.conversation :members: :undoc-members: :show-inheritance: Dialog ====== .. automodule:: telethon.tl.custom.dialog :members: :undoc-members: :show-inheritance: Draft ===== .. automodule:: telethon.tl.custom.draft :members: :undoc-members: :show-inheritance: File ==== .. automodule:: telethon.tl.custom.file :members: :undoc-members: :show-inheritance: Forward ======= .. automodule:: telethon.tl.custom.forward :members: :undoc-members: :show-inheritance: InlineBuilder ============= .. automodule:: telethon.tl.custom.inlinebuilder :members: :undoc-members: :show-inheritance: InlineResult ============ .. automodule:: telethon.tl.custom.inlineresult :members: :undoc-members: :show-inheritance: InlineResults ============= .. automodule:: telethon.tl.custom.inlineresults :members: :undoc-members: :show-inheritance: Message ======= .. automodule:: telethon.tl.custom.message :members: :undoc-members: :show-inheritance: MessageButton ============= .. automodule:: telethon.tl.custom.messagebutton :members: :undoc-members: :show-inheritance: ParticipantPermissions ====================== .. automodule:: telethon.tl.custom.participantpermissions :members: :undoc-members: :show-inheritance: QRLogin ======= .. automodule:: telethon.tl.custom.qrlogin :members: :undoc-members: :show-inheritance: SenderGetter ============ .. automodule:: telethon.tl.custom.sendergetter :members: :undoc-members: :show-inheritance: Telethon-1.39.0/readthedocs/modules/errors.rst000066400000000000000000000007171475566265000214160ustar00rootroot00000000000000.. _telethon-errors: ========== API Errors ========== These are the base errors that Telegram's API may raise. See :ref:`rpc-errors` for a more in-depth explanation on how to handle all known possible errors and learning to determine what a method may raise. .. automodule:: telethon.errors.common :members: :undoc-members: :show-inheritance: .. automodule:: telethon.errors.rpcbaseerrors :members: :undoc-members: :show-inheritance: Telethon-1.39.0/readthedocs/modules/events.rst000066400000000000000000000026261475566265000214070ustar00rootroot00000000000000.. _telethon-events: ============= Update Events ============= .. currentmodule:: telethon.events Every event (builder) subclasses `common.EventBuilder`, so all the methods in it can be used from any event builder/event instance. .. automodule:: telethon.events.common :members: :undoc-members: :show-inheritance: .. automodule:: telethon.events.newmessage :members: :undoc-members: :show-inheritance: .. automodule:: telethon.events.chataction :members: :undoc-members: :show-inheritance: .. automodule:: telethon.events.userupdate :members: :undoc-members: :show-inheritance: .. automodule:: telethon.events.messageedited :members: :undoc-members: :show-inheritance: .. automodule:: telethon.events.messagedeleted :members: :undoc-members: :show-inheritance: .. automodule:: telethon.events.messageread :members: :undoc-members: :show-inheritance: .. automodule:: telethon.events.callbackquery :members: :undoc-members: :show-inheritance: .. automodule:: telethon.events.inlinequery :members: :undoc-members: :show-inheritance: .. automodule:: telethon.events.album :members: :undoc-members: :show-inheritance: .. automodule:: telethon.events.raw :members: :undoc-members: :show-inheritance: .. automodule:: telethon.events :members: :undoc-members: :show-inheritance: Telethon-1.39.0/readthedocs/modules/helpers.rst000066400000000000000000000001631475566265000215370ustar00rootroot00000000000000======= Helpers ======= .. automodule:: telethon.helpers :members: :undoc-members: :show-inheritance: Telethon-1.39.0/readthedocs/modules/network.rst000066400000000000000000000013651475566265000215730ustar00rootroot00000000000000.. _telethon-network: ================ Connection Modes ================ The only part about network that you should worry about are the different connection modes, which are the following: .. automodule:: telethon.network.connection.tcpfull :members: :undoc-members: :show-inheritance: .. automodule:: telethon.network.connection.tcpabridged :members: :undoc-members: :show-inheritance: .. automodule:: telethon.network.connection.tcpintermediate :members: :undoc-members: :show-inheritance: .. automodule:: telethon.network.connection.tcpobfuscated :members: :undoc-members: :show-inheritance: .. automodule:: telethon.network.connection.http :members: :undoc-members: :show-inheritance: Telethon-1.39.0/readthedocs/modules/sessions.rst000066400000000000000000000010121475566265000217350ustar00rootroot00000000000000.. _telethon-sessions: ======== Sessions ======== These are the different built-in session storage that you may subclass. .. automodule:: telethon.sessions.abstract :members: :undoc-members: :show-inheritance: .. automodule:: telethon.sessions.memory :members: :undoc-members: :show-inheritance: .. automodule:: telethon.sessions.sqlite :members: :undoc-members: :show-inheritance: .. automodule:: telethon.sessions.string :members: :undoc-members: :show-inheritance: Telethon-1.39.0/readthedocs/modules/utils.rst000066400000000000000000000003041475566265000212320ustar00rootroot00000000000000.. _telethon-utils: ========= Utilities ========= These are the utilities that the library has to offer. .. automodule:: telethon.utils :members: :undoc-members: :show-inheritance: Telethon-1.39.0/readthedocs/quick-references/000077500000000000000000000000001475566265000211265ustar00rootroot00000000000000Telethon-1.39.0/readthedocs/quick-references/client-reference.rst000066400000000000000000000061261475566265000250770ustar00rootroot00000000000000.. _client-ref: ================ Client Reference ================ This page contains a summary of all the important methods and properties that you may need when using Telethon. They are sorted by relevance and are not in alphabetical order. You should use this page to learn about which methods are available, and if you need a usage example or further description of the arguments, be sure to follow the links. .. contents:: TelegramClient ============== This is a summary of the methods and properties you will find at :ref:`telethon-client`. Auth ---- .. currentmodule:: telethon.client.auth.AuthMethods .. autosummary:: :nosignatures: start send_code_request sign_in qr_login log_out edit_2fa Base ---- .. py:currentmodule:: telethon.client.telegrambaseclient.TelegramBaseClient .. autosummary:: :nosignatures: connect disconnect is_connected disconnected loop set_proxy Messages -------- .. py:currentmodule:: telethon.client.messages.MessageMethods .. autosummary:: :nosignatures: send_message edit_message delete_messages forward_messages iter_messages get_messages pin_message unpin_message send_read_acknowledge Uploads ------- .. py:currentmodule:: telethon.client.uploads.UploadMethods .. autosummary:: :nosignatures: send_file upload_file Downloads --------- .. currentmodule:: telethon.client.downloads.DownloadMethods .. autosummary:: :nosignatures: download_media download_profile_photo download_file iter_download Dialogs ------- .. py:currentmodule:: telethon.client.dialogs.DialogMethods .. autosummary:: :nosignatures: iter_dialogs get_dialogs edit_folder iter_drafts get_drafts delete_dialog conversation Users ----- .. py:currentmodule:: telethon.client.users.UserMethods .. autosummary:: :nosignatures: get_me is_bot is_user_authorized get_entity get_input_entity get_peer_id Chats ----- .. currentmodule:: telethon.client.chats.ChatMethods .. autosummary:: :nosignatures: iter_participants get_participants kick_participant iter_admin_log get_admin_log iter_profile_photos get_profile_photos edit_admin edit_permissions get_permissions get_stats action Parse Mode ---------- .. py:currentmodule:: telethon.client.messageparse.MessageParseMethods .. autosummary:: :nosignatures: parse_mode Updates ------- .. py:currentmodule:: telethon.client.updates.UpdateMethods .. autosummary:: :nosignatures: on run_until_disconnected add_event_handler remove_event_handler list_event_handlers catch_up set_receive_updates Bots ---- .. currentmodule:: telethon.client.bots.BotMethods .. autosummary:: :nosignatures: inline_query Buttons ------- .. currentmodule:: telethon.client.buttons.ButtonMethods .. autosummary:: :nosignatures: build_reply_markup Account ------- .. currentmodule:: telethon.client.account.AccountMethods .. autosummary:: :nosignatures: takeout end_takeout Telethon-1.39.0/readthedocs/quick-references/events-reference.rst000066400000000000000000000121031475566265000251150ustar00rootroot00000000000000================ Events Reference ================ Here you will find a quick summary of all the methods and properties that you can access when working with events. You can access the client that creates this event by doing ``event.client``, and you should view the description of the events to find out what arguments it allows on creation and its **attributes** (the properties will be shown here). .. important:: Remember that **all events base** `ChatGetter `! Please see :ref:`faq` if you don't know what this means or the implications of it. .. contents:: NewMessage ========== Occurs whenever a new text message or a message with media arrives. .. note:: The new message event **should be treated as** a normal `Message `, with the following exceptions: * ``pattern_match`` is the match object returned by ``pattern=``. * ``message`` is **not** the message string. It's the `Message ` object. Remember, this event is just a proxy over the message, so while you won't see its attributes and properties, you can still access them. Please see the full documentation for examples. Full documentation for the `NewMessage `. MessageEdited ============= Occurs whenever a message is edited. Just like `NewMessage `, you should treat this event as a `Message `. Full documentation for the `MessageEdited `. MessageDeleted ============== Occurs whenever a message is deleted. Note that this event isn't 100% reliable, since Telegram doesn't always notify the clients that a message was deleted. It only has the ``deleted_id`` and ``deleted_ids`` attributes (in addition to the chat if the deletion happened in a channel). Full documentation for the `MessageDeleted `. MessageRead =========== Occurs whenever one or more messages are read in a chat. Full documentation for the `MessageRead `. .. currentmodule:: telethon.events.messageread.MessageRead.Event .. autosummary:: :nosignatures: inbox message_ids get_messages is_read ChatAction ========== Occurs on certain chat actions, such as chat title changes, user join or leaves, pinned messages, photo changes, etc. Full documentation for the `ChatAction `. .. currentmodule:: telethon.events.chataction.ChatAction.Event .. autosummary:: :nosignatures: added_by kicked_by user input_user user_id users input_users user_ids respond reply delete get_pinned_message get_added_by get_kicked_by get_user get_input_user get_users get_input_users UserUpdate ========== Occurs whenever a user goes online, starts typing, etc. Full documentation for the `UserUpdate `. .. currentmodule:: telethon.events.userupdate.UserUpdate.Event .. autosummary:: :nosignatures: user input_user user_id get_user get_input_user typing uploading recording playing cancel geo audio round video contact document photo last_seen until online recently within_weeks within_months CallbackQuery ============= Occurs whenever you sign in as a bot and a user clicks one of the inline buttons on your messages. Full documentation for the `CallbackQuery `. .. currentmodule:: telethon.events.callbackquery.CallbackQuery.Event .. autosummary:: :nosignatures: id message_id data chat_instance via_inline respond reply edit delete answer get_message InlineQuery =========== Occurs whenever you sign in as a bot and a user sends an inline query such as ``@bot query``. Full documentation for the `InlineQuery `. .. currentmodule:: telethon.events.inlinequery.InlineQuery.Event .. autosummary:: :nosignatures: id text offset geo builder answer Album ===== Occurs whenever you receive an entire album. Full documentation for the `Album `. .. currentmodule:: telethon.events.album.Album.Event .. autosummary:: :nosignatures: grouped_id text raw_text is_reply forward get_reply_message respond reply forward_to edit delete mark_read pin Raw === Raw events are not actual events. Instead, they are the raw :tl:`Update` object that Telegram sends. You normally shouldn't need these. Telethon-1.39.0/readthedocs/quick-references/faq.rst000066400000000000000000000411321475566265000224300ustar00rootroot00000000000000.. _faq: === FAQ === Let's start the quick references section with some useful tips to keep in mind, with the hope that you will understand why certain things work the way that they do. .. contents:: Code without errors doesn't work ================================ Then it probably has errors, but you haven't enabled logging yet. To enable logging, at the following code to the top of your main file: .. code-block:: python import logging logging.basicConfig(format='[%(levelname) %(asctime)s] %(name)s: %(message)s', level=logging.WARNING) You can change the logging level to be something different, from less to more information: .. code-block:: python level=logging.CRITICAL # won't show errors (same as disabled) level=logging.ERROR # will only show errors that you didn't handle level=logging.WARNING # will also show messages with medium severity, such as internal Telegram issues level=logging.INFO # will also show informational messages, such as connection or disconnections level=logging.DEBUG # will show a lot of output to help debugging issues in the library See the official Python documentation for more information on logging_. How can I except FloodWaitError? ================================ You can use all errors from the API by importing: .. code-block:: python from telethon import errors And except them as such: .. code-block:: python try: await client.send_message(chat, 'Hi') except errors.FloodWaitError as e: # e.seconds is how many seconds you have # to wait before making the request again. print('Flood for', e.seconds) My account was deleted/limited when using the library ===================================================== First and foremost, **this is not a problem exclusive to Telethon. Any third-party library is prone to cause the accounts to appear banned.** Even official applications can make Telegram ban an account under certain circumstances. Third-party libraries such as Telethon are a lot easier to use, and as such, they are misused to spam, which causes Telegram to learn certain patterns and ban suspicious activity. There is no point in Telethon trying to circumvent this. Even if it succeeded, spammers would then abuse the library again, and the cycle would repeat. The library will only do things that you tell it to do. If you use the library with bad intentions, Telegram will hopefully ban you. However, you may also be part of a limited country, such as Iran or Russia. In that case, 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. We have also had reports from Kazakhstan and China, where connecting would fail. To solve these connection problems, you should use a proxy. Telegram may also ban virtual (VoIP) phone numbers, as again, they're likely to be used for spam. More recently (year 2023 onwards), Telegram has started putting a lot more measures to prevent spam (with even additions such as anonymous participants in groups or the inability to fetch group members at all). This means some of the anti-spam measures have gotten more aggressive. The recommendation has usually been to use the library only on well-established accounts (and not an account you just created), and to not perform actions that could be seen as abuse. Telegram decides what those actions are, and they're free to change how they operate at any time. 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`_. How can I use a proxy? ====================== This was one of the first things described in :ref:`signing-in`. How do I access a field? ======================== This is basic Python knowledge. You should use the dot operator: .. code-block:: python me = await client.get_me() print(me.username) # ^ we used the dot operator to access the username attribute result = await client(functions.photos.GetUserPhotosRequest( user_id='me', offset=0, max_id=0, limit=100 )) # Working with list is also pretty basic print(result.photos[0].sizes[-1].type) # ^ ^ ^ ^ ^ # | | | | \ type # | | | \ last size # | | \ list of sizes # access | \ first photo from the list # the... \ list of photos # # To print all, you could do (or mix-and-match): for photo in result.photos: for size in photo.sizes: print(size.type) AttributeError: 'coroutine' object has no attribute 'id' ======================================================== You either forgot to: .. code-block:: python import telethon.sync # ^^^^^ import sync Or: .. code-block:: python async def handler(event): me = await client.get_me() # ^^^^^ note the await print(me.username) sqlite3.OperationalError: database is locked ============================================ An older process is still running and is using the same ``'session'`` file. This error occurs when **two or more clients use the same session**, that is, when you write the same session name to be used in the client: * You have an older process using the same session file. * You have two different scripts running (interactive sessions count too). * You have two clients in the same script running at the same time. The solution is, if you need two clients, use two sessions. If the problem persists and you're on Linux, you can use ``fuser my.session`` to find out the process locking the file. As a last resort, you can reboot your system. If you really dislike SQLite, use a different session storage. There is an entire section covering that at :ref:`sessions`. event.chat or event.sender is None ================================== Telegram doesn't always send this information in order to save bandwidth. If you need the information, you should fetch it yourself, since the library won't do unnecessary work unless you need to: .. code-block:: python async def handler(event): chat = await event.get_chat() sender = await event.get_sender() File download is slow or sending files takes too long ===================================================== The communication with Telegram is encrypted. Encryption requires a lot of math, and doing it in pure Python is very slow. ``cryptg`` is a library which containns the encryption functions used by Telethon. If it is installed (via ``pip install cryptg``), it will automatically be used and should provide a considerable speed boost. You can know whether it's used by configuring ``logging`` (at ``INFO`` level or lower) *before* importing ``telethon``. Note that the library does *not* download or upload files in parallel, which can also help with the speed of downloading or uploading a single file. There are snippets online implementing that. The reason why this is not built-in is because the limiting factor in the long run are ``FloodWaitError``, and using parallel download or uploads only makes them occur sooner. What does "Server sent a very new message with ID" mean? ======================================================== You may also see this error as "Server sent a very old message with ID". This is a security feature from Telethon that cannot be disabled and is meant to protect you against replay attacks. When this message is incorrectly reported as a "bug", the most common patterns seem to be: * Your system time is incorrect. * The proxy you're using may be interfering somehow. * The Telethon session is being used or has been used from somewhere else. Make sure that you created the session from Telethon, and are not using the same session anywhere else. If you need to use the same account from multiple places, login and use a different session for each place you need. What does "Server replied with a wrong session ID" mean? ======================================================== This is a security feature from Telethon that cannot be disabled and is meant to protect you against unwanted session reuse. When this message is reported as a "bug", the most common patterns seem to be: * The proxy you're using may be interfering somehow. * The Telethon session is being used or has been used from somewhere else. Make sure that you created the session from Telethon, and are not using the same session anywhere else. If you need to use the same account from multiple places, login and use a different session for each place you need. * You may be using multiple connections to the Telegram server, which seems to confuse Telegram. Most of the time it should be safe to ignore this warning. If the library still doesn't behave correctly, make sure to check if any of the above bullet points applies in your case and try to work around it. If the issue persists and there is a way to reliably reproduce this error, please add a comment with any additional details you can provide to `issue 3759`_, and perhaps some additional investigation can be done (but it's unlikely, as Telegram *is* sending unexpected data). What does "Could not find a matching Constructor ID for the TLObject" mean? =========================================================================== Telegram uses "layers", which you can think of as "versions" of the API they offer. When Telethon reads responses that the Telegram servers send, these need to be deserialized (into what Telethon calls "TLObjects"). Every Telethon version understands a single Telegram layer. When Telethon connects to Telegram, both agree on the layer to use. If the layers don't match, Telegram may send certain objects which Telethon no longer understands. When this message is reported as a "bug", the most common patterns seem to be that the Telethon session is being used or has been used from somewhere else. Make sure that you created the session from Telethon, and are not using the same session anywhere else. If you need to use the same account from multiple places, login and use a different session for each place you need. What does "Task was destroyed but it is pending" mean? ====================================================== Your script likely finished abruptly, the ``asyncio`` event loop got destroyed, and the library did not get a chance to properly close the connection and close the session. Make sure you're either using the context manager for the client or always call ``await client.disconnect()`` (by e.g. using a ``try/finally``). What does "The asyncio event loop must not change after connection" mean? ========================================================================= Telethon uses ``asyncio``, and makes use of things like tasks and queues internally to manage the connection to the server and match responses to the requests you make. Most of them are initialized after the client is connected. For example, if the library expects a result to a request made in loop A, but you attempt to get that result in loop B, you will very likely find a deadlock. To avoid a deadlock, the library checks to make sure the loop in use is the same as the one used to initialize everything, and if not, it throws an error. The most common cause is ``asyncio.run``, since it creates a new event loop. If you ``asyncio.run`` a function to create the client and set it up, and then you ``asyncio.run`` another function to do work, things won't work, so the library throws an error early to let you know something is wrong. Instead, it's often a good idea to have a single ``async def main`` and simply ``asyncio.run()`` it and do all the work there. From it, you're also able to call other ``async def`` without having to touch ``asyncio.run`` again: .. code-block:: python # It's fine to create the client outside as long as you don't connect client = TelegramClient(...) async def main(): # Now the client will connect, so the loop must not change from now on. # But as long as you do all the work inside main, including calling # other async functions, things will work. async with client: .... if __name__ == '__main__': asyncio.run(main()) Be sure to read the ``asyncio`` documentation if you want a better understanding of event loop, tasks, and what functions you can use. What does "bases ChatGetter" mean? ================================== In Python, classes can base others. This is called `inheritance `_. What it means is that "if a class bases another, you can use the other's methods too". For example, `Message ` *bases* `ChatGetter `. In turn, `ChatGetter ` defines things like `obj.chat_id `. So if you have a message, you can access that too: .. code-block:: python # ChatGetter has a chat_id property, and Message bases ChatGetter. # Thus you can use ChatGetter properties and methods from Message print(message.chat_id) Telegram has a lot to offer, and inheritance helps the library reduce boilerplate, so it's important to know this concept. For newcomers, this may be a problem, so we explain what it means here in the FAQ. Can I send files by ID? ======================= When people talk about IDs, they often refer to one of two things: the integer ID inside media, and a random-looking long string. You cannot use the integer ID to send media. Generally speaking, sending media requires a combination of ID, ``access_hash`` and ``file_reference``. The first two are integers, while the last one is a random ``bytes`` sequence. * The integer ``id`` will always be the same for every account, so every user or bot looking at a particular media file, will see a consistent ID. * The ``access_hash`` will always be the same for a given account, but different accounts will each see their own, different ``access_hash``. This makes it impossible to get media object from one account and use it in another. The other account must fetch the media object itself. * The ``file_reference`` is random for everyone and will only work for a few hours before it expires. It must be refetched before the media can be used (to either resend the media or download it). The second type of "`file ID `_" people refer to is a concept from the HTTP Bot API. It's a custom format which encodes enough information to use the media. Telethon provides an old version of these HTTP Bot API-style file IDs via ``message.file.id``, however, this feature is no longer maintained, so it may not work. It will be removed in future versions. Nonetheless, it is possible to find a different Python package (or write your own) to parse these file IDs and construct the necessary input file objects to send or download the media. Can I use Flask with the library? ================================= Yes, if you know what you are doing. However, you will probably have a lot of headaches to get threads and asyncio to work together. Instead, consider using `Quart `_, an asyncio-based alternative to `Flask `_. Check out `quart_login.py`_ for an example web-application based on Quart. Can I use Anaconda/Spyder/IPython with the library? =================================================== Yes, but these interpreters run the asyncio event loop implicitly, which interferes with the ``telethon.sync`` magic module. If you use them, you should **not** import ``sync``: .. code-block:: python # Change any of these...: from telethon import TelegramClient, sync, ... from telethon.sync import TelegramClient, ... # ...with this: from telethon import TelegramClient, ... You are also more likely to get "sqlite3.OperationalError: database is locked" with them. If they cause too much trouble, just write your code in a ``.py`` file and run that, or use the normal ``python`` interpreter. .. _logging: https://docs.python.org/3/library/logging.html .. _@SpamBot: https://t.me/SpamBot .. _issue 297: https://github.com/LonamiWebs/Telethon/issues/297 .. _issue 3759: https://github.com/LonamiWebs/Telethon/issues/3759 .. _quart_login.py: https://github.com/LonamiWebs/Telethon/tree/v1/telethon_examples#quart_loginpy Telethon-1.39.0/readthedocs/quick-references/objects-reference.rst000066400000000000000000000153621475566265000252540ustar00rootroot00000000000000================= Objects Reference ================= This is the quick reference for those objects returned by client methods or other useful modules that the library has to offer. They are kept in a separate page to help finding and discovering them. Remember that this page only shows properties and methods, **not attributes**. Make sure to open the full documentation to find out about the attributes. .. contents:: ChatGetter ========== All events base `ChatGetter `, and some of the objects below do too, so it's important to know its methods. .. currentmodule:: telethon.tl.custom.chatgetter.ChatGetter .. autosummary:: :nosignatures: chat input_chat chat_id is_private is_group is_channel get_chat get_input_chat SenderGetter ============ Similar to `ChatGetter `, a `SenderGetter ` is the same, but it works for senders instead. .. currentmodule:: telethon.tl.custom.sendergetter.SenderGetter .. autosummary:: :nosignatures: sender input_sender sender_id get_sender get_input_sender Message ======= .. currentmodule:: telethon.tl.custom.message The `Message` type is very important, mostly because we are working with a library for a *messaging* platform, so messages are widely used: in events, when fetching history, replies, etc. It bases `ChatGetter ` and `SenderGetter `. Properties ---------- .. note:: We document *custom properties* here, not all the attributes of the `Message` (which is the information Telegram actually returns). .. currentmodule:: telethon.tl.custom.message.Message .. autosummary:: :nosignatures: text raw_text is_reply forward buttons button_count file photo document web_preview audio voice video video_note gif sticker contact game geo invoice poll venue action_entities via_bot via_input_bot client Methods ------- .. autosummary:: :nosignatures: respond reply forward_to edit delete get_reply_message click mark_read pin download_media get_entities_text get_buttons File ==== The `File ` type is a wrapper object returned by `Message.file `, and you can use it to easily access a document's attributes, such as its name, bot-API style file ID, etc. .. currentmodule:: telethon.tl.custom.file.File .. autosummary:: :nosignatures: id name ext mime_type width height size duration title performer emoji sticker_set Conversation ============ The `Conversation ` object is returned by the `client.conversation() ` method to easily send and receive responses like a normal conversation. It bases `ChatGetter `. .. currentmodule:: telethon.tl.custom.conversation.Conversation .. autosummary:: :nosignatures: send_message send_file mark_read get_response get_reply get_edit wait_read wait_event cancel cancel_all AdminLogEvent ============= The `AdminLogEvent ` object is returned by the `client.iter_admin_log() ` method to easily iterate over past "events" (deleted messages, edits, title changes, leaving members…) These are all the properties you can find in it: .. currentmodule:: telethon.tl.custom.adminlogevent.AdminLogEvent .. autosummary:: :nosignatures: id date user_id action old new changed_about changed_title changed_username changed_photo changed_sticker_set changed_message deleted_message changed_admin changed_restrictions changed_invites joined joined_invite left changed_hide_history changed_signatures changed_pin changed_default_banned_rights stopped_poll Button ====== The `Button ` class is used when you login as a bot account to send messages with reply markup, such as inline buttons or custom keyboards. These are the static methods you can use to create instances of the markup: .. currentmodule:: telethon.tl.custom.button.Button .. autosummary:: :nosignatures: inline switch_inline url auth text request_location request_phone request_poll clear force_reply InlineResult ============ The `InlineResult ` object is returned inside a list by the `client.inline_query() ` method to make an inline query to a bot that supports being used in inline mode, such as `@like `_. Note that the list returned is in fact a *subclass* of a list called `InlineResults `, which, in addition of being a list (iterator, indexed access, etc.), has extra attributes and methods. These are the constants for the types, properties and methods you can find the individual results: .. currentmodule:: telethon.tl.custom.inlineresult.InlineResult .. autosummary:: :nosignatures: ARTICLE PHOTO GIF VIDEO VIDEO_GIF AUDIO DOCUMENT LOCATION VENUE CONTACT GAME type message title description url photo document click download_media Dialog ====== The `Dialog ` object is returned when you call `client.iter_dialogs() `. .. currentmodule:: telethon.tl.custom.dialog.Dialog .. autosummary:: :nosignatures: send_message archive delete Draft ====== The `Draft ` object is returned when you call `client.iter_drafts() `. .. currentmodule:: telethon.tl.custom.draft.Draft .. autosummary:: :nosignatures: entity input_entity get_entity get_input_entity text raw_text is_empty set_message send delete Utils ===== The `telethon.utils` module has plenty of methods that make using the library a lot easier. Only the interesting ones will be listed here. .. currentmodule:: telethon.utils .. autosummary:: :nosignatures: get_display_name get_extension get_inner_text get_peer_id resolve_id pack_bot_file_id resolve_bot_file_id resolve_invite_link Telethon-1.39.0/readthedocs/requirements.txt000066400000000000000000000000331475566265000211530ustar00rootroot00000000000000./ sphinx-rtd-theme~=1.3.0 Telethon-1.39.0/requirements.txt000066400000000000000000000000121475566265000166630ustar00rootroot00000000000000pyaes rsa Telethon-1.39.0/setup.py000077500000000000000000000213101475566265000151200ustar00rootroot00000000000000#!/usr/bin/env python3 """A setuptools based setup module. See: https://packaging.python.org/en/latest/distributing.html https://github.com/pypa/sampleproject Extra supported commands are: * gen, to generate the classes required for Telethon to run or docs * pypi, to generate sdist, bdist_wheel, and push to PyPi """ import itertools import json import os import re import shutil import sys import urllib.request from pathlib import Path from subprocess import run from setuptools import find_packages, setup # Needed since we're importing local files sys.path.insert(0, os.path.dirname(__file__)) class TempWorkDir: """Switches the working directory to be the one on which this file lives, while within the 'with' block. """ def __init__(self, new=None): self.original = None self.new = new or str(Path(__file__).parent.resolve()) def __enter__(self): # os.chdir does not work with Path in Python 3.5.x self.original = str(Path('.').resolve()) os.makedirs(self.new, exist_ok=True) os.chdir(self.new) return self def __exit__(self, *args): os.chdir(self.original) API_REF_URL = 'https://tl.telethon.dev/' GENERATOR_DIR = Path('telethon_generator') LIBRARY_DIR = Path('telethon') ERRORS_IN = GENERATOR_DIR / 'data/errors.csv' ERRORS_OUT = LIBRARY_DIR / 'errors/rpcerrorlist.py' METHODS_IN = GENERATOR_DIR / 'data/methods.csv' # Which raw API methods are covered by *friendly* methods in the client? FRIENDLY_IN = GENERATOR_DIR / 'data/friendly.csv' TLOBJECT_IN_TLS = [Path(x) for x in GENERATOR_DIR.glob('data/*.tl')] TLOBJECT_OUT = LIBRARY_DIR / 'tl' IMPORT_DEPTH = 2 DOCS_IN_RES = GENERATOR_DIR / 'data/html' DOCS_OUT = Path('docs') def generate(which, action='gen'): from telethon_generator.parsers import\ parse_errors, parse_methods, parse_tl, find_layer from telethon_generator.generators import\ generate_errors, generate_tlobjects, generate_docs, clean_tlobjects layer = next(filter(None, map(find_layer, TLOBJECT_IN_TLS))) errors = list(parse_errors(ERRORS_IN)) methods = list(parse_methods(METHODS_IN, FRIENDLY_IN, {e.str_code: e for e in errors})) tlobjects = list(itertools.chain(*( parse_tl(file, layer, methods) for file in TLOBJECT_IN_TLS))) if not which: which.extend(('tl', 'errors')) clean = action == 'clean' action = 'Cleaning' if clean else 'Generating' if 'all' in which: which.remove('all') for x in ('tl', 'errors', 'docs'): if x not in which: which.append(x) if 'tl' in which: which.remove('tl') print(action, 'TLObjects...') if clean: clean_tlobjects(TLOBJECT_OUT) else: generate_tlobjects(tlobjects, layer, IMPORT_DEPTH, TLOBJECT_OUT) if 'errors' in which: which.remove('errors') print(action, 'RPCErrors...') if clean: if ERRORS_OUT.is_file(): ERRORS_OUT.unlink() else: with ERRORS_OUT.open('w') as file: generate_errors(errors, file) if 'docs' in which: which.remove('docs') print(action, 'documentation...') if clean: if DOCS_OUT.is_dir(): shutil.rmtree(str(DOCS_OUT)) else: in_path = DOCS_IN_RES.resolve() with TempWorkDir(DOCS_OUT): generate_docs(tlobjects, methods, layer, in_path) if 'json' in which: which.remove('json') print(action, 'JSON schema...') json_files = [x.with_suffix('.json') for x in TLOBJECT_IN_TLS] if clean: for file in json_files: if file.is_file(): file.unlink() else: def gen_json(fin, fout): meths = [] constructors = [] for tl in parse_tl(fin, layer): if tl.is_function: meths.append(tl.to_dict()) else: constructors.append(tl.to_dict()) what = {'constructors': constructors, 'methods': meths} with open(fout, 'w') as f: json.dump(what, f, indent=2) for fs in zip(TLOBJECT_IN_TLS, json_files): gen_json(*fs) if which: print( 'The following items were not understood:', which, '\n Consider using only "tl", "errors" and/or "docs".' '\n Using only "clean" will clean them. "all" to act on all.' '\n For instance "gen tl errors".' ) def main(argv): if len(argv) >= 2 and argv[1] in ('gen', 'clean'): generate(argv[2:], argv[1]) elif len(argv) >= 2 and argv[1] == 'pypi': # Make sure tl.telethon.dev is up-to-date first with urllib.request.urlopen(API_REF_URL) as resp: html = resp.read() m = re.search(br'layer\s+(\d+)', html) if not m: print('Failed to check that the API reference is up to date:', API_REF_URL) return from telethon_generator.parsers import find_layer layer = next(filter(None, map(find_layer, TLOBJECT_IN_TLS))) published_layer = int(m[1]) if published_layer != layer: print('Published layer', published_layer, 'does not match current layer', layer, '.') print('Make sure to update the API reference site first:', API_REF_URL) return # (Re)generate the code to make sure we don't push without it generate(['tl', 'errors']) # Try importing the telethon module to assert it has no errors try: import telethon except Exception as e: print('Packaging for PyPi aborted, importing the module failed.') print(e) return remove_dirs = ['__pycache__', 'build', 'dist', 'Telethon.egg-info'] for root, _dirs, _files in os.walk(LIBRARY_DIR, topdown=False): # setuptools is including __pycache__ for some reason (#1605) if root.endswith('/__pycache__'): remove_dirs.append(root) for x in remove_dirs: shutil.rmtree(x, ignore_errors=True) run('python3 setup.py sdist', shell=True) run('python3 setup.py bdist_wheel', shell=True) run('twine upload dist/*', shell=True) for x in ('build', 'dist', 'Telethon.egg-info'): shutil.rmtree(x, ignore_errors=True) else: # e.g. install from GitHub if GENERATOR_DIR.is_dir(): generate(['tl', 'errors']) # Get the long description from the README file with open('README.rst', 'r', encoding='utf-8') as f: long_description = f.read() with open('telethon/version.py', 'r', encoding='utf-8') as f: version = re.search(r"^__version__\s*=\s*'(.*)'.*$", f.read(), flags=re.MULTILINE).group(1) setup( name='Telethon', version=version, description="Full-featured Telegram client library for Python 3", long_description=long_description, url='https://github.com/LonamiWebs/Telethon', download_url='https://github.com/LonamiWebs/Telethon/releases', author='Lonami Exo', author_email='totufals@hotmail.com', license='MIT', # See https://stackoverflow.com/a/40300957/4759433 # -> https://www.python.org/dev/peps/pep-0345/#requires-python # -> http://setuptools.readthedocs.io/en/latest/setuptools.html python_requires='>=3.5', # See https://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[ # 3 - Alpha # 4 - Beta # 5 - Production/Stable 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Topic :: Communications :: Chat', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', ], keywords='telegram api chat client library messaging mtproto', packages=find_packages(exclude=[ 'telethon_*', 'tests*' ]), install_requires=['pyaes', 'rsa'], extras_require={ 'cryptg': ['cryptg'] } ) if __name__ == '__main__': with TempWorkDir(): main(sys.argv) Telethon-1.39.0/telethon/000077500000000000000000000000001475566265000152305ustar00rootroot00000000000000Telethon-1.39.0/telethon/__init__.py000066400000000000000000000006271475566265000173460ustar00rootroot00000000000000from .client.telegramclient import TelegramClient from .network import connection from .tl.custom import Button from .tl import patched as _ # import for its side-effects from . import version, events, utils, errors, types, functions, custom __version__ = version.__version__ __all__ = [ 'TelegramClient', 'Button', 'types', 'functions', 'custom', 'errors', 'events', 'utils', 'connection' ] Telethon-1.39.0/telethon/_updates/000077500000000000000000000000001475566265000170345ustar00rootroot00000000000000Telethon-1.39.0/telethon/_updates/__init__.py000066400000000000000000000002521475566265000211440ustar00rootroot00000000000000from .entitycache import EntityCache from .messagebox import MessageBox, GapError, PrematureEndReason from .session import SessionState, ChannelState, Entity, EntityType Telethon-1.39.0/telethon/_updates/entitycache.py000066400000000000000000000034751475566265000217170ustar00rootroot00000000000000from .session import EntityType, Entity _sentinel = object() class EntityCache: def __init__( self, hash_map: dict = _sentinel, self_id: int = None, self_bot: bool = None ): self.hash_map = {} if hash_map is _sentinel else hash_map self.self_id = self_id self.self_bot = self_bot def set_self_user(self, id, bot, hash): self.self_id = id self.self_bot = bot if hash: self.hash_map[id] = (hash, EntityType.BOT if bot else EntityType.USER) def get(self, id): try: hash, ty = self.hash_map[id] return Entity(ty, id, hash) except KeyError: return None def extend(self, users, chats): # See https://core.telegram.org/api/min for "issues" with "min constructors". self.hash_map.update( (u.id, ( u.access_hash, EntityType.BOT if u.bot else EntityType.USER, )) for u in users if getattr(u, 'access_hash', None) and not u.min ) self.hash_map.update( (c.id, ( c.access_hash, EntityType.MEGAGROUP if c.megagroup else ( EntityType.GIGAGROUP if getattr(c, 'gigagroup', None) else EntityType.CHANNEL ), )) for c in chats if getattr(c, 'access_hash', None) and not getattr(c, 'min', None) ) def get_all_entities(self): return [Entity(ty, id, hash) for id, (hash, ty) in self.hash_map.items()] def put(self, entity): self.hash_map[entity.id] = (entity.hash, entity.ty) def retain(self, filter): self.hash_map = {k: v for k, v in self.hash_map.items() if filter(k)} def __len__(self): return len(self.hash_map) Telethon-1.39.0/telethon/_updates/messagebox.py000066400000000000000000001041471475566265000215520ustar00rootroot00000000000000""" This module deals with correct handling of updates, including gaps, and knowing when the code should "get difference" (the set of updates that the client should know by now minus the set of updates that it actually knows). Each chat has its own [`Entry`] in the [`MessageBox`] (this `struct` is the "entry point"). At any given time, the message box may be either getting difference for them (entry is in [`MessageBox::getting_diff_for`]) or not. If not getting difference, a possible gap may be found for the updates (entry is in [`MessageBox::possible_gaps`]). Otherwise, the entry is on its happy path. Gaps are cleared when they are either resolved on their own (by waiting for a short time) or because we got the difference for the corresponding entry. While there are entries for which their difference must be fetched, [`MessageBox::check_deadlines`] will always return [`Instant::now`], since "now" is the time to get the difference. """ import asyncio import datetime import time import logging from enum import Enum from .session import SessionState, ChannelState from ..tl import types as tl, functions as fn from ..helpers import get_running_loop # Telegram sends `seq` equal to `0` when "it doesn't matter", so we use that value too. NO_SEQ = 0 # See https://core.telegram.org/method/updates.getChannelDifference. BOT_CHANNEL_DIFF_LIMIT = 100000 USER_CHANNEL_DIFF_LIMIT = 100 # > It may be useful to wait up to 0.5 seconds POSSIBLE_GAP_TIMEOUT = 0.5 # After how long without updates the client will "timeout". # # When this timeout occurs, the client will attempt to fetch updates by itself, ignoring all the # updates that arrive in the meantime. After all updates are fetched when this happens, the # client will resume normal operation, and the timeout will reset. # # Documentation recommends 15 minutes without updates (https://core.telegram.org/api/updates). NO_UPDATES_TIMEOUT = 15 * 60 # object() but with a tag to make it easier to debug class Sentinel: __slots__ = ('tag',) def __init__(self, tag=None): self.tag = tag or '_' def __repr__(self): return self.tag # Entry "enum". # Account-wide `pts` includes private conversations (one-to-one) and small group chats. ENTRY_ACCOUNT = Sentinel('ACCOUNT') # Account-wide `qts` includes only "secret" one-to-one chats. ENTRY_SECRET = Sentinel('SECRET') # Integers will be Channel-specific `pts`, and includes "megagroup", "broadcast" and "supergroup" channels. # Python's logging doesn't define a TRACE level. Pick halfway between DEBUG and NOTSET. # We don't define a name for this as libraries shouldn't do that though. LOG_LEVEL_TRACE = (logging.DEBUG - logging.NOTSET) // 2 _sentinel = Sentinel() def next_updates_deadline(): return get_running_loop().time() + NO_UPDATES_TIMEOUT def epoch(): return datetime.datetime(*time.gmtime(0)[:6]).replace(tzinfo=datetime.timezone.utc) class GapError(ValueError): def __repr__(self): return 'GapError()' class PrematureEndReason(Enum): TEMPORARY_SERVER_ISSUES = 'tmp' BANNED = 'ban' # Represents the information needed to correctly handle a specific `tl::enums::Update`. class PtsInfo: __slots__ = ('pts', 'pts_count', 'entry') def __init__( self, pts: int, pts_count: int, entry: object ): self.pts = pts self.pts_count = pts_count self.entry = entry @classmethod def from_update(cls, update): pts = getattr(update, 'pts', None) if pts: pts_count = getattr(update, 'pts_count', None) or 0 try: entry = update.message.peer_id.channel_id except AttributeError: entry = getattr(update, 'channel_id', None) or ENTRY_ACCOUNT return cls(pts=pts, pts_count=pts_count, entry=entry) qts = getattr(update, 'qts', None) if qts: return cls(pts=qts, pts_count=1, entry=ENTRY_SECRET) return None def __repr__(self): return f'PtsInfo(pts={self.pts}, pts_count={self.pts_count}, entry={self.entry})' # The state of a particular entry in the message box. class State: __slots__ = ('pts', 'deadline') def __init__( self, # Current local persistent timestamp. pts: int, # Next instant when we would get the update difference if no updates arrived before then. deadline: float ): self.pts = pts self.deadline = deadline def __repr__(self): return f'State(pts={self.pts}, deadline={self.deadline})' # > ### Recovering gaps # > […] Manually obtaining updates is also required in the following situations: # > • Loss of sync: a gap was found in `seq` / `pts` / `qts` (as described above). # > It may be useful to wait up to 0.5 seconds in this situation and abort the sync in case a new update # > arrives, that fills the gap. # # This is really easy to trigger by spamming messages in a channel (with as little as 3 members works), because # the updates produced by the RPC request take a while to arrive (whereas the read update comes faster alone). class PossibleGap: __slots__ = ('deadline', 'updates') def __init__( self, deadline: float, # Pending updates (those with a larger PTS, producing the gap which may later be filled). updates: list # of updates ): self.deadline = deadline self.updates = updates def __repr__(self): return f'PossibleGap(deadline={self.deadline}, update_count={len(self.updates)})' # Represents a "message box" (event `pts` for a specific entry). # # See https://core.telegram.org/api/updates#message-related-event-sequences. class MessageBox: __slots__ = ('_log', 'map', 'date', 'seq', 'next_deadline', 'possible_gaps', 'getting_diff_for') def __init__( self, log, # Map each entry to their current state. map: dict = _sentinel, # entry -> state # Additional fields beyond PTS needed by `ENTRY_ACCOUNT`. date: datetime.datetime = epoch() + datetime.timedelta(seconds=1), seq: int = NO_SEQ, # Holds the entry with the closest deadline (optimization to avoid recalculating the minimum deadline). next_deadline: object = None, # entry # Which entries have a gap and may soon trigger a need to get difference. # # If a gap is found, stores the required information to resolve it (when should it timeout and what updates # should be held in case the gap is resolved on its own). # # Not stored directly in `map` as an optimization (else we would need another way of knowing which entries have # a gap in them). possible_gaps: dict = _sentinel, # entry -> possiblegap # For which entries are we currently getting difference. getting_diff_for: set = _sentinel, # entry ): self._log = log self.map = {} if map is _sentinel else map self.date = date self.seq = seq self.next_deadline = next_deadline self.possible_gaps = {} if possible_gaps is _sentinel else possible_gaps self.getting_diff_for = set() if getting_diff_for is _sentinel else getting_diff_for if __debug__: self._trace('MessageBox initialized') def _trace(self, msg, *args, **kwargs): # Calls to trace can't really be removed beforehand without some dark magic. # So every call to trace is prefixed with `if __debug__`` instead, to remove # it when using `python -O`. Probably unnecessary, but it's nice to avoid # paying the cost for something that is not used. self._log.log(LOG_LEVEL_TRACE, 'Current MessageBox state: seq = %r, date = %s, map = %r', self.seq, self.date.isoformat(), self.map) self._log.log(LOG_LEVEL_TRACE, msg, *args, **kwargs) # region Creation, querying, and setting base state. def load(self, session_state, channel_states): """ Create a [`MessageBox`] from a previously known update state. """ if __debug__: self._trace('Loading MessageBox with session_state = %r, channel_states = %r', session_state, channel_states) deadline = next_updates_deadline() self.map.clear() if session_state.pts != NO_SEQ: self.map[ENTRY_ACCOUNT] = State(pts=session_state.pts, deadline=deadline) if session_state.qts != NO_SEQ: self.map[ENTRY_SECRET] = State(pts=session_state.qts, deadline=deadline) self.map.update((s.channel_id, State(pts=s.pts, deadline=deadline)) for s in channel_states) self.date = datetime.datetime.fromtimestamp(session_state.date, tz=datetime.timezone.utc) self.seq = session_state.seq self.next_deadline = ENTRY_ACCOUNT def session_state(self): """ Return the current state. This should be used for persisting the state. """ return dict( pts=self.map[ENTRY_ACCOUNT].pts if ENTRY_ACCOUNT in self.map else NO_SEQ, qts=self.map[ENTRY_SECRET].pts if ENTRY_SECRET in self.map else NO_SEQ, date=self.date, seq=self.seq, ), {id: state.pts for id, state in self.map.items() if isinstance(id, int)} def is_empty(self) -> bool: """ Return true if the message box is empty and has no state yet. """ return ENTRY_ACCOUNT not in self.map def check_deadlines(self): """ Return the next deadline when receiving updates should timeout. If a deadline expired, the corresponding entries will be marked as needing to get its difference. While there are entries pending of getting their difference, this method returns the current instant. """ now = get_running_loop().time() if self.getting_diff_for: return now deadline = next_updates_deadline() # Most of the time there will be zero or one gap in flight so finding the minimum is cheap. if self.possible_gaps: deadline = min(deadline, *(gap.deadline for gap in self.possible_gaps.values())) elif self.next_deadline in self.map: deadline = min(deadline, self.map[self.next_deadline].deadline) # asyncio's loop time precision only seems to be about 3 decimal places, so it's possible that # we find the same number again on repeated calls. Without the "or equal" part we would log the # timeout for updates several times (it also makes sense to get difference if now is the deadline). if now >= deadline: # Check all expired entries and add them to the list that needs getting difference. self.getting_diff_for.update(entry for entry, gap in self.possible_gaps.items() if now >= gap.deadline) self.getting_diff_for.update(entry for entry, state in self.map.items() if now >= state.deadline) if __debug__: self._trace('Deadlines met, now getting diff for %r', self.getting_diff_for) # When extending `getting_diff_for`, it's important to have the moral equivalent of # `begin_get_diff` (that is, clear possible gaps if we're now getting difference). for entry in self.getting_diff_for: self.possible_gaps.pop(entry, None) return deadline # Reset the deadline for the periods without updates for the given entries. # # It also updates the next deadline time to reflect the new closest deadline. def reset_deadlines(self, entries, deadline): if not entries: return for entry in entries: if entry not in self.map: raise RuntimeError('Called reset_deadline on an entry for which we do not have state') self.map[entry].deadline = deadline if self.next_deadline in entries: # If the updated deadline was the closest one, recalculate the new minimum. self.next_deadline = min(self.map.items(), key=lambda entry_state: entry_state[1].deadline)[0] elif self.next_deadline in self.map and deadline < self.map[self.next_deadline].deadline: # If the updated deadline is smaller than the next deadline, change the next deadline to be the new one. # Any entry will do, so the one from the last iteration is fine. self.next_deadline = entry # else an unrelated deadline was updated, so the closest one remains unchanged. # Convenience to reset a channel's deadline, with optional timeout. def reset_channel_deadline(self, channel_id, timeout): self.reset_deadlines({channel_id}, get_running_loop().time() + (timeout or NO_UPDATES_TIMEOUT)) # Sets the update state. # # Should be called right after login if [`MessageBox::new`] was used, otherwise undesirable # updates will be fetched. def set_state(self, state, reset=True): if __debug__: self._trace('Setting state %s', state) deadline = next_updates_deadline() if state.pts != NO_SEQ or not reset: self.map[ENTRY_ACCOUNT] = State(pts=state.pts, deadline=deadline) else: self.map.pop(ENTRY_ACCOUNT, None) # Telegram seems to use the `qts` for bot accounts, but while applying difference, # it might be reset back to 0. See issue #3873 for more details. # # During login, a value of zero would mean the `pts` is unknown, # so the map shouldn't contain that entry. # But while applying difference, if the value is zero, it (probably) # truly means that's what should be used (hence the `reset` flag). if state.qts != NO_SEQ or not reset: self.map[ENTRY_SECRET] = State(pts=state.qts, deadline=deadline) else: self.map.pop(ENTRY_SECRET, None) self.date = state.date self.seq = state.seq # Like [`MessageBox::set_state`], but for channels. Useful when getting dialogs. # # The update state will only be updated if no entry was known previously. def try_set_channel_state(self, id, pts): if __debug__: self._trace('Trying to set channel state for %r: %r', id, pts) if id not in self.map: self.map[id] = State(pts=pts, deadline=next_updates_deadline()) # Try to begin getting difference for the given entry. # Fails if the entry does not have a previously-known state that can be used to get its difference. # # Clears any previous gaps. def try_begin_get_diff(self, entry, reason): if entry not in self.map: # Won't actually be able to get difference for this entry if we don't have a pts to start off from. if entry in self.possible_gaps: raise RuntimeError('Should not have a possible_gap for an entry not in the state map') if __debug__: self._trace('Should get difference for %r because %s but cannot due to missing hash', entry, reason) return if __debug__: self._trace('Marking %r as needing difference because %s', entry, reason) self.getting_diff_for.add(entry) self.possible_gaps.pop(entry, None) # Finish getting difference for the given entry. # # It also resets the deadline. def end_get_diff(self, entry): try: self.getting_diff_for.remove(entry) except KeyError: raise RuntimeError('Called end_get_diff on an entry which was not getting diff for') self.reset_deadlines({entry}, next_updates_deadline()) assert entry not in self.possible_gaps, "gaps shouldn't be created while getting difference" # endregion Creation, querying, and setting base state. # region "Normal" updates flow (processing and detection of gaps). # Process an update and return what should be done with it. # # Updates corresponding to entries for which their difference is currently being fetched # will be ignored. While according to the [updates' documentation]: # # > Implementations [have] to postpone updates received via the socket while # > filling gaps in the event and `Update` sequences, as well as avoid filling # > gaps in the same sequence. # # In practice, these updates should have also been retrieved through getting difference. # # [updates documentation] https://core.telegram.org/api/updates def process_updates( self, updates, chat_hashes, result, # out list of updates; returns list of user, chat, or raise if gap ): # v1 has never sent updates produced by the client itself to the handlers. # However proper update handling requires those to be processed. # This is an ugly workaround for that. self_outgoing = getattr(updates, '_self_outgoing', False) real_result = result result = [] date = getattr(updates, 'date', None) seq = getattr(updates, 'seq', None) seq_start = getattr(updates, 'seq_start', None) users = getattr(updates, 'users', None) or [] chats = getattr(updates, 'chats', None) or [] if __debug__: self._trace('Processing updates with seq = %r, seq_start = %r, date = %s: %s', seq, seq_start, date.isoformat() if date else None, updates) if date is None: # updatesTooLong is the only one with no date (we treat it as a gap) self.try_begin_get_diff(ENTRY_ACCOUNT, 'received updatesTooLong') raise GapError if seq is None: seq = NO_SEQ if seq_start is None: seq_start = seq # updateShort is the only update which cannot be dispatched directly but doesn't have 'updates' field updates = getattr(updates, 'updates', None) or [updates.update if isinstance(updates, tl.UpdateShort) else updates] for u in updates: u._self_outgoing = self_outgoing # > For all the other [not `updates` or `updatesCombined`] `Updates` type constructors # > there is no need to check `seq` or change a local state. if seq_start != NO_SEQ: if self.seq + 1 > seq_start: # Skipping updates that were already handled if __debug__: self._trace('Skipping updates as they should have already been handled') return (users, chats) elif self.seq + 1 < seq_start: # Gap detected self.try_begin_get_diff(ENTRY_ACCOUNT, 'detected gap') raise GapError # else apply def _sort_gaps(update): pts = PtsInfo.from_update(update) return pts.pts - pts.pts_count if pts else 0 reset_deadlines = set() # temporary buffer any_pts_applied = [False] # using a list to pass "by reference" result.extend(filter(None, ( self.apply_pts_info(u, reset_deadlines=reset_deadlines, any_pts_applied=any_pts_applied) # Telegram can send updates out of order (e.g. ReadChannelInbox first # and then NewChannelMessage, both with the same pts, but the count is # 0 and 1 respectively), so we sort them first. for u in sorted(updates, key=_sort_gaps)))) # > If the updates were applied, local *Updates* state must be updated # > with `seq` (unless it's 0) and `date` from the constructor. # # By "were applied", we assume it means "some other pts was applied". # Updates which can be applied in any order, such as `UpdateChat`, # should not cause `seq` to be updated (or upcoming updates such as # `UpdateChatParticipant` could be missed). if any_pts_applied[0]: if __debug__: self._trace('Updating seq as local pts was updated too') if date != epoch(): self.date = date if seq != NO_SEQ: self.seq = seq self.reset_deadlines(reset_deadlines, next_updates_deadline()) if self.possible_gaps: if __debug__: self._trace('Trying to re-apply %r possible gaps', len(self.possible_gaps)) # For each update in possible gaps, see if the gap has been resolved already. for key in list(self.possible_gaps.keys()): self.possible_gaps[key].updates.sort(key=_sort_gaps) for _ in range(len(self.possible_gaps[key].updates)): update = self.possible_gaps[key].updates.pop(0) # If this fails to apply, it will get re-inserted at the end. # All should fail, so the order will be preserved (it would've cycled once). update = self.apply_pts_info(update, reset_deadlines=None) if update: result.append(update) if __debug__: self._trace('Resolved gap with %r: %s', PtsInfo.from_update(update), update) # Clear now-empty gaps. self.possible_gaps = {entry: gap for entry, gap in self.possible_gaps.items() if gap.updates} real_result.extend(u for u in result if not u._self_outgoing) return (users, chats) # Tries to apply the input update if its `PtsInfo` follows the correct order. # # If the update can be applied, it is returned; otherwise, the update is stored in a # possible gap (unless it was already handled or would be handled through getting # difference) and `None` is returned. def apply_pts_info( self, update, *, reset_deadlines, any_pts_applied=[True], # mutable default is fine as it's write-only ): # This update means we need to call getChannelDifference to get the updates from the channel if isinstance(update, tl.UpdateChannelTooLong): self.try_begin_get_diff(update.channel_id, 'received updateChannelTooLong') return None pts = PtsInfo.from_update(update) if not pts: # No pts means that the update can be applied in any order. if __debug__: self._trace('No pts in update, so it can be applied in any order: %s', update) return update # As soon as we receive an update of any form related to messages (has `PtsInfo`), # the "no updates" period for that entry is reset. # # Build the `HashSet` to avoid calling `reset_deadline` more than once for the same entry. # # By the time this method returns, self.map will have an entry for which we can reset its deadline. if reset_deadlines: reset_deadlines.add(pts.entry) if pts.entry in self.getting_diff_for: # Note: early returning here also prevents gap from being inserted (which they should # not be while getting difference). if __debug__: self._trace('Skipping update with %r as its difference is being fetched', pts) return None if pts.entry in self.map: local_pts = self.map[pts.entry].pts if local_pts + pts.pts_count > pts.pts: # Ignore if __debug__: self._trace('Skipping update since local pts %r > %r: %s', local_pts, pts, update) return None elif local_pts + pts.pts_count < pts.pts: # Possible gap # TODO store chats too? if __debug__: self._trace('Possible gap since local pts %r < %r: %s', local_pts, pts, update) if pts.entry not in self.possible_gaps: self.possible_gaps[pts.entry] = PossibleGap( deadline=get_running_loop().time() + POSSIBLE_GAP_TIMEOUT, updates=[] ) self.possible_gaps[pts.entry].updates.append(update) return None else: # Apply any_pts_applied[0] = True if __debug__: self._trace('Applying update pts since local pts %r = %r: %s', local_pts, pts, update) # In a channel, we may immediately receive: # * ReadChannelInbox (pts = X, pts_count = 0) # * NewChannelMessage (pts = X, pts_count = 1) # # Notice how both `pts` are the same. If they were to be applied out of order, the first # one however would've triggered a gap because `local_pts` + `pts_count` of 0 would be # less than `remote_pts`. So there is no risk by setting the `local_pts` to match the # `remote_pts` here of missing the new message. # # The message would however be lost if we initialized the pts with the first one, since # the second one would appear "already handled". To prevent this we set the pts to be # one less when the count is 0 (which might be wrong and trigger a gap later on, but is # unlikely). This will prevent us from losing updates in the unlikely scenario where these # two updates arrive in different packets (and therefore couldn't be sorted beforehand). if pts.entry in self.map: self.map[pts.entry].pts = pts.pts else: # When a chat is migrated to a megagroup, the first update can be a `ReadChannelInbox` # with `pts = 1, pts_count = 0` followed by a `NewChannelMessage` with `pts = 2, pts_count=1`. # Note how the `pts` for the message is 2 and not 1 unlike the case described before! # This is likely because the `pts` cannot be 0 (or it would fail with PERSISTENT_TIMESTAMP_EMPTY), # which forces the first update to be 1. But if we got difference with 1 and the second update # also used 1, we would miss it, so Telegram probably uses 2 to work around that. self.map[pts.entry] = State( pts=(pts.pts - (0 if pts.pts_count else 1)) or 1, deadline=next_updates_deadline() ) return update # endregion "Normal" updates flow (processing and detection of gaps). # region Getting and applying account difference. # Return the request that needs to be made to get the difference, if any. def get_difference(self): for entry in (ENTRY_ACCOUNT, ENTRY_SECRET): if entry in self.getting_diff_for: if entry not in self.map: raise RuntimeError('Should not try to get difference for an entry without known state') gd = fn.updates.GetDifferenceRequest( pts=self.map[ENTRY_ACCOUNT].pts, pts_total_limit=None, date=self.date, qts=self.map[ENTRY_SECRET].pts if ENTRY_SECRET in self.map else NO_SEQ, ) if __debug__: self._trace('Requesting account difference %s', gd) return gd return None # Similar to [`MessageBox::process_updates`], but using the result from getting difference. def apply_difference( self, diff, chat_hashes, ): if __debug__: self._trace('Applying account difference %s', diff) finish = None result = None if isinstance(diff, tl.updates.DifferenceEmpty): finish = True self.date = diff.date self.seq = diff.seq result = [], [], [] elif isinstance(diff, tl.updates.Difference): finish = True chat_hashes.extend(diff.users, diff.chats) result = self.apply_difference_type(diff, chat_hashes) elif isinstance(diff, tl.updates.DifferenceSlice): finish = False chat_hashes.extend(diff.users, diff.chats) result = self.apply_difference_type(diff, chat_hashes) elif isinstance(diff, tl.updates.DifferenceTooLong): finish = True self.map[ENTRY_ACCOUNT].pts = diff.pts # the deadline will be reset once the diff ends result = [], [], [] if finish: account = ENTRY_ACCOUNT in self.getting_diff_for secret = ENTRY_SECRET in self.getting_diff_for if not account and not secret: raise RuntimeError('Should not be applying the difference when neither account or secret was diff was active') # Both may be active if both expired at the same time. if account: self.end_get_diff(ENTRY_ACCOUNT) if secret: self.end_get_diff(ENTRY_SECRET) return result def apply_difference_type( self, diff, chat_hashes, ): state = getattr(diff, 'intermediate_state', None) or diff.state self.set_state(state, reset=False) # diff.other_updates can contain things like UpdateChannelTooLong and UpdateNewChannelMessage. # We need to process those as if they were socket updates to discard any we have already handled. updates = [] self.process_updates(tl.Updates( updates=diff.other_updates, users=diff.users, chats=diff.chats, date=epoch(), seq=NO_SEQ, # this way date is not used ), chat_hashes, updates) updates.extend(tl.UpdateNewMessage( message=m, pts=NO_SEQ, pts_count=NO_SEQ, ) for m in diff.new_messages) updates.extend(tl.UpdateNewEncryptedMessage( message=m, qts=NO_SEQ, ) for m in diff.new_encrypted_messages) return updates, diff.users, diff.chats def end_difference(self): if __debug__: self._trace('Ending account difference') account = ENTRY_ACCOUNT in self.getting_diff_for secret = ENTRY_SECRET in self.getting_diff_for if not account and not secret: raise RuntimeError('Should not be ending get difference when neither account or secret was diff was active') # Both may be active if both expired at the same time. if account: self.end_get_diff(ENTRY_ACCOUNT) if secret: self.end_get_diff(ENTRY_SECRET) # endregion Getting and applying account difference. # region Getting and applying channel difference. # Return the request that needs to be made to get a channel's difference, if any. def get_channel_difference( self, chat_hashes, ): entry = next((id for id in self.getting_diff_for if isinstance(id, int)), None) if not entry: return None packed = chat_hashes.get(entry) if not packed: # Cannot get channel difference as we're missing its hash # TODO we should probably log this self.end_get_diff(entry) # Remove the outdated `pts` entry from the map so that the next update can correct # it. Otherwise, it will spam that the access hash is missing. self.map.pop(entry, None) return None state = self.map.get(entry) if not state: raise RuntimeError('Should not try to get difference for an entry without known state') gd = fn.updates.GetChannelDifferenceRequest( force=False, channel=tl.InputChannel(packed.id, packed.hash), filter=tl.ChannelMessagesFilterEmpty(), pts=state.pts, limit=BOT_CHANNEL_DIFF_LIMIT if chat_hashes.self_bot else USER_CHANNEL_DIFF_LIMIT ) if __debug__: self._trace('Requesting channel difference %s', gd) return gd # Similar to [`MessageBox::process_updates`], but using the result from getting difference. def apply_channel_difference( self, request, diff, chat_hashes, ): entry = request.channel.channel_id if __debug__: self._trace('Applying channel difference for %r: %s', entry, diff) self.possible_gaps.pop(entry, None) if isinstance(diff, tl.updates.ChannelDifferenceEmpty): assert diff.final self.end_get_diff(entry) self.map[entry].pts = diff.pts return [], [], [] elif isinstance(diff, tl.updates.ChannelDifferenceTooLong): assert diff.final self.map[entry].pts = diff.dialog.pts chat_hashes.extend(diff.users, diff.chats) self.reset_channel_deadline(entry, diff.timeout) # This `diff` has the "latest messages and corresponding chats", but it would # be strange to give the user only partial changes of these when they would # expect all updates to be fetched. Instead, nothing is returned. return [], [], [] elif isinstance(diff, tl.updates.ChannelDifference): if diff.final: self.end_get_diff(entry) self.map[entry].pts = diff.pts chat_hashes.extend(diff.users, diff.chats) updates = [] self.process_updates(tl.Updates( updates=diff.other_updates, users=diff.users, chats=diff.chats, date=epoch(), seq=NO_SEQ, # this way date is not used ), chat_hashes, updates) updates.extend(tl.UpdateNewChannelMessage( message=m, pts=NO_SEQ, pts_count=NO_SEQ, ) for m in diff.new_messages) self.reset_channel_deadline(entry, None) return updates, diff.users, diff.chats def end_channel_difference(self, request, reason: PrematureEndReason, chat_hashes): entry = request.channel.channel_id if __debug__: self._trace('Ending channel difference for %r because %s', entry, reason) if reason == PrematureEndReason.TEMPORARY_SERVER_ISSUES: # Temporary issues. End getting difference without updating the pts so we can retry later. self.possible_gaps.pop(entry, None) self.end_get_diff(entry) elif reason == PrematureEndReason.BANNED: # Banned in the channel. Forget its state since we can no longer fetch updates from it. self.possible_gaps.pop(entry, None) self.end_get_diff(entry) del self.map[entry] else: raise RuntimeError('Unknown reason to end channel difference') # endregion Getting and applying channel difference. Telethon-1.39.0/telethon/_updates/session.py000066400000000000000000000140301475566265000210670ustar00rootroot00000000000000from typing import Optional, Tuple from enum import IntEnum from ..tl.types import InputPeerUser, InputPeerChat, InputPeerChannel class SessionState: """ Stores the information needed to fetch updates and about the current user. * user_id: 64-bit number representing the user identifier. * dc_id: 32-bit number relating to the datacenter identifier where the user is. * bot: is the logged-in user a bot? * pts: 64-bit number holding the state needed to fetch updates. * qts: alternative 64-bit number holding the state needed to fetch updates. * date: 64-bit number holding the date needed to fetch updates. * seq: 64-bit-number holding the sequence number needed to fetch updates. * takeout_id: 64-bit-number holding the identifier of the current takeout session. Note that some of the numbers will only use 32 out of the 64 available bits. However, for future-proofing reasons, we recommend you pretend they are 64-bit long. """ __slots__ = ('user_id', 'dc_id', 'bot', 'pts', 'qts', 'date', 'seq', 'takeout_id') def __init__( self, user_id: int, dc_id: int, bot: bool, pts: int, qts: int, date: int, seq: int, takeout_id: Optional[int] ): self.user_id = user_id self.dc_id = dc_id self.bot = bot self.pts = pts self.qts = qts self.date = date self.seq = seq self.takeout_id = takeout_id def __repr__(self): return repr({k: getattr(self, k) for k in self.__slots__}) class ChannelState: """ Stores the information needed to fetch updates from a channel. * channel_id: 64-bit number representing the channel identifier. * pts: 64-bit number holding the state needed to fetch updates. """ __slots__ = ('channel_id', 'pts') def __init__( self, channel_id: int, pts: int, ): self.channel_id = channel_id self.pts = pts def __repr__(self): return repr({k: getattr(self, k) for k in self.__slots__}) class EntityType(IntEnum): """ You can rely on the type value to be equal to the ASCII character one of: * 'U' (85): this entity belongs to a :tl:`User` who is not a ``bot``. * 'B' (66): this entity belongs to a :tl:`User` who is a ``bot``. * 'G' (71): this entity belongs to a small group :tl:`Chat`. * 'C' (67): this entity belongs to a standard broadcast :tl:`Channel`. * 'M' (77): this entity belongs to a megagroup :tl:`Channel`. * 'E' (69): this entity belongs to an "enormous" "gigagroup" :tl:`Channel`. """ USER = ord('U') BOT = ord('B') GROUP = ord('G') CHANNEL = ord('C') MEGAGROUP = ord('M') GIGAGROUP = ord('E') def canonical(self): """ Return the canonical version of this type. """ return _canon_entity_types[self] _canon_entity_types = { EntityType.USER: EntityType.USER, EntityType.BOT: EntityType.USER, EntityType.GROUP: EntityType.GROUP, EntityType.CHANNEL: EntityType.CHANNEL, EntityType.MEGAGROUP: EntityType.CHANNEL, EntityType.GIGAGROUP: EntityType.CHANNEL, } class Entity: """ Stores the information needed to use a certain user, chat or channel with the API. * ty: 8-bit number indicating the type of the entity (of type `EntityType`). * id: 64-bit number uniquely identifying the entity among those of the same type. * hash: 64-bit signed number needed to use this entity with the API. The string representation of this class is considered to be stable, for as long as Telegram doesn't need to add more fields to the entities. It can also be converted to bytes with ``bytes(entity)``, for a more compact representation. """ __slots__ = ('ty', 'id', 'hash') def __init__( self, ty: EntityType, id: int, hash: int ): self.ty = ty self.id = id self.hash = hash @property def is_user(self): """ ``True`` if the entity is either a user or a bot. """ return self.ty in (EntityType.USER, EntityType.BOT) @property def is_group(self): """ ``True`` if the entity is a small group chat or `megagroup`_. .. _megagroup: https://telegram.org/blog/supergroups5k """ return self.ty in (EntityType.GROUP, EntityType.MEGAGROUP) @property def is_broadcast(self): """ ``True`` if the entity is a broadcast channel or `broadcast group`_. .. _broadcast group: https://telegram.org/blog/autodelete-inv2#groups-with-unlimited-members """ return self.ty in (EntityType.CHANNEL, EntityType.GIGAGROUP) @classmethod def from_str(cls, string: str): """ Convert the string into an `Entity`. """ try: ty, id, hash = string.split('.') ty, id, hash = ord(ty), int(id), int(hash) except AttributeError: raise TypeError(f'expected str, got {string!r}') from None except (TypeError, ValueError): raise ValueError(f'malformed entity str (must be T.id.hash), got {string!r}') from None return cls(EntityType(ty), id, hash) @classmethod def from_bytes(cls, blob): """ Convert the bytes into an `Entity`. """ try: ty, id, hash = struct.unpack(' 'TelegramClient': """ Returns a :ref:`telethon-client` which calls methods behind a takeout session. It does so by creating a proxy object over the current client through which making requests will use :tl:`InvokeWithTakeoutRequest` to wrap them. In other words, returns the current client modified so that requests are done as a takeout: Some of the calls made through the takeout session will have lower flood limits. This is useful if you want to export the data from conversations or mass-download media, since the rate limits will be lower. Only some requests will be affected, and you will need to adjust the `wait_time` of methods like `client.iter_messages `. By default, all parameters are `None`, and you need to enable those you plan to use by setting them to either `True` or `False`. You should ``except errors.TakeoutInitDelayError as e``, since this exception will raise depending on the condition of the session. You can then access ``e.seconds`` to know how long you should wait for before calling the method again. There's also a `success` property available in the takeout proxy object, so from the `with` body you can set the boolean result that will be sent back to Telegram. But if it's left `None` as by default, then the action is based on the `finalize` parameter. If it's `True` then the takeout will be finished, and if no exception occurred during it, then `True` will be considered as a result. Otherwise, the takeout will not be finished and its ID will be preserved for future usage as `client.session.takeout_id `. Arguments finalize (`bool`): Whether the takeout session should be finalized upon exit or not. contacts (`bool`): Set to `True` if you plan on downloading contacts. users (`bool`): Set to `True` if you plan on downloading information from users and their private conversations with you. chats (`bool`): Set to `True` if you plan on downloading information from small group chats, such as messages and media. megagroups (`bool`): Set to `True` if you plan on downloading information from megagroups (channels), such as messages and media. channels (`bool`): Set to `True` if you plan on downloading information from broadcast channels, such as messages and media. files (`bool`): Set to `True` if you plan on downloading media and you don't only wish to export messages. max_file_size (`int`): The maximum file size, in bytes, that you plan to download for each message with media. Example .. code-block:: python from telethon import errors try: async with client.takeout() as takeout: await client.get_messages('me') # normal call await takeout.get_messages('me') # wrapped through takeout (less limits) async for message in takeout.iter_messages(chat, wait_time=0): ... # Do something with the message except errors.TakeoutInitDelayError as e: print('Must wait', e.seconds, 'before takeout') """ request_kwargs = dict( contacts=contacts, message_users=users, message_chats=chats, message_megagroups=megagroups, message_channels=channels, files=files, file_max_size=max_file_size ) arg_specified = (arg is not None for arg in request_kwargs.values()) if self.session.takeout_id is None or any(arg_specified): request = functions.account.InitTakeoutSessionRequest( **request_kwargs) else: request = None return _TakeoutClient(finalize, self, request) async def end_takeout(self: 'TelegramClient', success: bool) -> bool: """ Finishes the current takeout session. Arguments success (`bool`): Whether the takeout completed successfully or not. Returns `True` if the operation was successful, `False` otherwise. Example .. code-block:: python await client.end_takeout(success=False) """ try: async with _TakeoutClient(True, self, None) as takeout: takeout.success = success except ValueError: return False return True Telethon-1.39.0/telethon/client/auth.py000066400000000000000000000606121475566265000200260ustar00rootroot00000000000000import getpass import inspect import os import sys import typing import warnings from .. import utils, helpers, errors, password as pwd_mod from ..tl import types, functions, custom from .._updates import SessionState if typing.TYPE_CHECKING: from .telegramclient import TelegramClient class AuthMethods: # region Public methods def start( self: 'TelegramClient', phone: typing.Union[typing.Callable[[], str], str] = lambda: input('Please enter your phone (or bot token): '), password: typing.Union[typing.Callable[[], str], str] = lambda: getpass.getpass('Please enter your password: '), *, bot_token: str = None, force_sms: bool = False, code_callback: typing.Callable[[], typing.Union[str, int]] = None, first_name: str = 'New User', last_name: str = '', max_attempts: int = 3) -> 'TelegramClient': """ Starts the client (connects and logs in if necessary). By default, this method will be interactive (asking for user input if needed), and will handle 2FA if enabled too. If the event loop is already running, this method returns a coroutine that you should await on your own code; otherwise the loop is ran until said coroutine completes. Arguments phone (`str` | `int` | `callable`): The phone (or callable without arguments to get it) to which the code will be sent. If a bot-token-like string is given, it will be used as such instead. The argument may be a coroutine. password (`str`, `callable`, optional): The password for 2 Factor Authentication (2FA). This is only required if it is enabled in your account. The argument may be a coroutine. bot_token (`str`): Bot Token obtained by `@BotFather `_ to log in as a bot. Cannot be specified with ``phone`` (only one of either allowed). force_sms (`bool`, optional): Whether to force sending the code request as SMS. This only makes sense when signing in with a `phone`. code_callback (`callable`, optional): A callable that will be used to retrieve the Telegram login code. Defaults to `input()`. The argument may be a coroutine. first_name (`str`, optional): The first name to be used if signing up. This has no effect if the account already exists and you sign in. last_name (`str`, optional): Similar to the first name, but for the last. Optional. max_attempts (`int`, optional): How many times the code/password callback should be retried or switching between signing in and signing up. Returns This `TelegramClient`, so initialization can be chained with ``.start()``. Example .. code-block:: python client = TelegramClient('anon', api_id, api_hash) # Starting as a bot account await client.start(bot_token=bot_token) # Starting as a user account await client.start(phone) # Please enter the code you received: 12345 # Please enter your password: ******* # (You are now logged in) # Starting using a context manager (this calls start()): with client: pass """ if code_callback is None: def code_callback(): return input('Please enter the code you received: ') elif not callable(code_callback): raise ValueError( 'The code_callback parameter needs to be a callable ' 'function that returns the code you received by Telegram.' ) if not phone and not bot_token: raise ValueError('No phone number or bot token provided.') if phone and bot_token and not callable(phone): raise ValueError('Both a phone and a bot token provided, ' 'must only provide one of either') coro = self._start( phone=phone, password=password, bot_token=bot_token, force_sms=force_sms, code_callback=code_callback, first_name=first_name, last_name=last_name, max_attempts=max_attempts ) return ( coro if self.loop.is_running() else self.loop.run_until_complete(coro) ) async def _start( self: 'TelegramClient', phone, password, bot_token, force_sms, code_callback, first_name, last_name, max_attempts): if not self.is_connected(): await self.connect() # Rather than using `is_user_authorized`, use `get_me`. While this is # more expensive and needs to retrieve more data from the server, it # enables the library to warn users trying to login to a different # account. See #1172. me = await self.get_me() if me is not None: # The warnings here are on a best-effort and may fail. if bot_token: # bot_token's first part has the bot ID, but it may be invalid # so don't try to parse as int (instead cast our ID to string). if bot_token[:bot_token.find(':')] != str(me.id): warnings.warn( 'the session already had an authorized user so it did ' 'not login to the bot account using the provided ' 'bot_token (it may not be using the user you expect)' ) elif phone and not callable(phone) and utils.parse_phone(phone) != me.phone: warnings.warn( 'the session already had an authorized user so it did ' 'not login to the user account using the provided ' 'phone (it may not be using the user you expect)' ) return self if not bot_token: # Turn the callable into a valid phone number (or bot token) while callable(phone): value = phone() if inspect.isawaitable(value): value = await value if ':' in value: # Bot tokens have 'user_id:access_hash' format bot_token = value break phone = utils.parse_phone(value) or phone if bot_token: await self.sign_in(bot_token=bot_token) return self me = None attempts = 0 two_step_detected = False await self.send_code_request(phone, force_sms=force_sms) while attempts < max_attempts: try: value = code_callback() if inspect.isawaitable(value): value = await value # Since sign-in with no code works (it sends the code) # we must double-check that here. Else we'll assume we # logged in, and it will return None as the User. if not value: raise errors.PhoneCodeEmptyError(request=None) # Raises SessionPasswordNeededError if 2FA enabled me = await self.sign_in(phone, code=value) break except errors.SessionPasswordNeededError: two_step_detected = True break except (errors.PhoneCodeEmptyError, errors.PhoneCodeExpiredError, errors.PhoneCodeHashEmptyError, errors.PhoneCodeInvalidError): print('Invalid code. Please try again.', file=sys.stderr) attempts += 1 else: raise RuntimeError( '{} consecutive sign-in attempts failed. Aborting' .format(max_attempts) ) if two_step_detected: if not password: raise ValueError( "Two-step verification is enabled for this account. " "Please provide the 'password' argument to 'start()'." ) if callable(password): for _ in range(max_attempts): try: value = password() if inspect.isawaitable(value): value = await value me = await self.sign_in(phone=phone, password=value) break except errors.PasswordHashInvalidError: print('Invalid password. Please try again', file=sys.stderr) else: raise errors.PasswordHashInvalidError(request=None) else: me = await self.sign_in(phone=phone, password=password) # We won't reach here if any step failed (exit by exception) signed, name = 'Signed in successfully as ', utils.get_display_name(me) tos = '; remember to not break the ToS or you will risk an account ban!' try: print(signed, name, tos, sep='') except UnicodeEncodeError: # Some terminals don't support certain characters print(signed, name.encode('utf-8', errors='ignore') .decode('ascii', errors='ignore'), tos, sep='') return self def _parse_phone_and_hash(self, phone, phone_hash): """ Helper method to both parse and validate phone and its hash. """ phone = utils.parse_phone(phone) or self._phone if not phone: raise ValueError( 'Please make sure to call send_code_request first.' ) phone_hash = phone_hash or self._phone_code_hash.get(phone, None) if not phone_hash: raise ValueError('You also need to provide a phone_code_hash.') return phone, phone_hash async def sign_in( self: 'TelegramClient', phone: str = None, code: typing.Union[str, int] = None, *, password: str = None, bot_token: str = None, phone_code_hash: str = None) -> 'typing.Union[types.User, types.auth.SentCode]': """ Logs in to Telegram to an existing user or bot account. You should only use this if you are not authorized yet. This method will send the code if it's not provided. .. note:: In most cases, you should simply use `start()` and not this method. Arguments phone (`str` | `int`): The phone to send the code to if no code was provided, or to override the phone that was previously used with these requests. code (`str` | `int`): The code that Telegram sent. Note that if you have sent this code through the application itself it will immediately expire. If you want to send the code, obfuscate it somehow. If you're not doing any of this you can ignore this note. password (`str`): 2FA password, should be used if a previous call raised ``SessionPasswordNeededError``. bot_token (`str`): Used to sign in as a bot. Not all requests will be available. This should be the hash the `@BotFather `_ gave you. phone_code_hash (`str`, optional): The hash returned by `send_code_request`. This can be left as `None` to use the last hash known for the phone to be used. Returns The signed in user, or the information about :meth:`send_code_request`. Example .. code-block:: python phone = '+34 123 123 123' await client.sign_in(phone) # send code code = input('enter code: ') await client.sign_in(phone, code) """ me = await self.get_me() if me: return me if phone and not code and not password: return await self.send_code_request(phone) elif code: phone, phone_code_hash = \ self._parse_phone_and_hash(phone, phone_code_hash) # May raise PhoneCodeEmptyError, PhoneCodeExpiredError, # PhoneCodeHashEmptyError or PhoneCodeInvalidError. request = functions.auth.SignInRequest( phone, phone_code_hash, str(code) ) elif password: pwd = await self(functions.account.GetPasswordRequest()) request = functions.auth.CheckPasswordRequest( pwd_mod.compute_check(pwd, password) ) elif bot_token: request = functions.auth.ImportBotAuthorizationRequest( flags=0, bot_auth_token=bot_token, api_id=self.api_id, api_hash=self.api_hash ) else: raise ValueError( 'You must provide a phone and a code the first time, ' 'and a password only if an RPCError was raised before.' ) try: result = await self(request) except errors.PhoneCodeExpiredError: self._phone_code_hash.pop(phone, None) raise if isinstance(result, types.auth.AuthorizationSignUpRequired): # Emulate pre-layer 104 behaviour self._tos = result.terms_of_service raise errors.PhoneNumberUnoccupiedError(request=request) return await self._on_login(result.user) async def sign_up( self: 'TelegramClient', code: typing.Union[str, int], first_name: str, last_name: str = '', *, phone: str = None, phone_code_hash: str = None) -> 'types.User': """ This method can no longer be used, and will immediately raise a ``ValueError``. See `issue #4050 `_ for context. """ raise ValueError('Third-party applications cannot sign up for Telegram. See https://github.com/LonamiWebs/Telethon/issues/4050 for details') async def _on_login(self, user): """ Callback called whenever the login or sign up process completes. Returns the input user parameter. """ self._mb_entity_cache.set_self_user(user.id, user.bot, user.access_hash) self._authorized = True state = await self(functions.updates.GetStateRequest()) self._message_box.load(SessionState(0, 0, 0, state.pts, state.qts, int(state.date.timestamp()), state.seq, 0), []) return user async def send_code_request( self: 'TelegramClient', phone: str, *, force_sms: bool = False, _retry_count: int = 0) -> 'types.auth.SentCode': """ Sends the Telegram code needed to login to the given phone number. Arguments phone (`str` | `int`): The phone to which the code will be sent. force_sms (`bool`, optional): Whether to force sending as SMS. This has been deprecated. See `issue #4050 `_ for context. Returns An instance of :tl:`SentCode`. Example .. code-block:: python phone = '+34 123 123 123' sent = await client.send_code_request(phone) print(sent) """ if force_sms: warnings.warn('force_sms has been deprecated and no longer works') force_sms = False result = None phone = utils.parse_phone(phone) or self._phone phone_hash = self._phone_code_hash.get(phone) if not phone_hash: try: result = await self(functions.auth.SendCodeRequest( phone, self.api_id, self.api_hash, types.CodeSettings())) except errors.AuthRestartError: if _retry_count > 2: raise return await self.send_code_request( phone, force_sms=force_sms, _retry_count=_retry_count+1) # TODO figure out when/if/how this can happen if isinstance(result, types.auth.SentCodeSuccess): raise RuntimeError('logged in right after sending the code') # If we already sent a SMS, do not resend the code (hash may be empty) if isinstance(result.type, types.auth.SentCodeTypeSms): force_sms = False # phone_code_hash may be empty, if it is, do not save it (#1283) if result.phone_code_hash: self._phone_code_hash[phone] = phone_hash = result.phone_code_hash else: force_sms = True self._phone = phone if force_sms: try: result = await self( functions.auth.ResendCodeRequest(phone, phone_hash)) except errors.PhoneCodeExpiredError: if _retry_count > 2: raise self._phone_code_hash.pop(phone, None) self._log[__name__].info( "Phone code expired in ResendCodeRequest, requesting a new code" ) return await self.send_code_request( phone, force_sms=False, _retry_count=_retry_count+1) if isinstance(result, types.auth.SentCodeSuccess): raise RuntimeError('logged in right after resending the code') self._phone_code_hash[phone] = result.phone_code_hash return result async def qr_login(self: 'TelegramClient', ignored_ids: typing.List[int] = None) -> custom.QRLogin: """ Initiates the QR login procedure. Note that you must be connected before invoking this, as with any other request. It is up to the caller to decide how to present the code to the user, whether it's the URL, using the token bytes directly, or generating a QR code and displaying it by other means. See the documentation for `QRLogin` to see how to proceed after this. Arguments ignored_ids (List[`int`]): List of already logged-in user IDs, to prevent logging in twice with the same user. Returns An instance of `QRLogin`. Example .. code-block:: python def display_url_as_qr(url): pass # do whatever to show url as a qr to the user qr_login = await client.qr_login() display_url_as_qr(qr_login.url) # Important! You need to wait for the login to complete! await qr_login.wait() # If you have 2FA enabled, `wait` will raise `telethon.errors.SessionPasswordNeededError`. # You should except that error and call `sign_in` with the password if this happens. """ qr_login = custom.QRLogin(self, ignored_ids or []) await qr_login.recreate() return qr_login async def log_out(self: 'TelegramClient') -> bool: """ Logs out Telegram and deletes the current ``*.session`` file. The client is unusable after logging out and a new instance should be created. Returns `True` if the operation was successful. Example .. code-block:: python # Note: you will need to login again! await client.log_out() """ try: await self(functions.auth.LogOutRequest()) except errors.RPCError: return False self._mb_entity_cache.set_self_user(None, None, None) self._authorized = False await self.disconnect() self.session.delete() self.session = None return True async def edit_2fa( self: 'TelegramClient', current_password: str = None, new_password: str = None, *, hint: str = '', email: str = None, email_code_callback: typing.Callable[[int], str] = None) -> bool: """ Changes the 2FA settings of the logged in user. Review carefully the parameter explanations before using this method. Note that this method may be *incredibly* slow depending on the prime numbers that must be used during the process to make sure that everything is safe. Has no effect if both current and new password are omitted. Arguments current_password (`str`, optional): The current password, to authorize changing to ``new_password``. Must be set if changing existing 2FA settings. Must **not** be set if 2FA is currently disabled. Passing this by itself will remove 2FA (if correct). new_password (`str`, optional): The password to set as 2FA. If 2FA was already enabled, ``current_password`` **must** be set. Leaving this blank or `None` will remove the password. hint (`str`, optional): Hint to be displayed by Telegram when it asks for 2FA. Leaving unspecified is highly discouraged. Has no effect if ``new_password`` is not set. email (`str`, optional): Recovery and verification email. If present, you must also set `email_code_callback`, else it raises ``ValueError``. email_code_callback (`callable`, optional): If an email is provided, a callback that returns the code sent to it must also be set. This callback may be asynchronous. It should return a string with the code. The length of the code will be passed to the callback as an input parameter. If the callback returns an invalid code, it will raise ``CodeInvalidError``. Returns `True` if successful, `False` otherwise. Example .. code-block:: python # Setting a password for your account which didn't have await client.edit_2fa(new_password='I_<3_Telethon') # Removing the password await client.edit_2fa(current_password='I_<3_Telethon') """ if new_password is None and current_password is None: return False if email and not callable(email_code_callback): raise ValueError('email present without email_code_callback') pwd = await self(functions.account.GetPasswordRequest()) pwd.new_algo.salt1 += os.urandom(32) assert isinstance(pwd, types.account.Password) if not pwd.has_password and current_password: current_password = None if current_password: password = pwd_mod.compute_check(pwd, current_password) else: password = types.InputCheckPasswordEmpty() if new_password: new_password_hash = pwd_mod.compute_digest( pwd.new_algo, new_password) else: new_password_hash = b'' try: await self(functions.account.UpdatePasswordSettingsRequest( password=password, new_settings=types.account.PasswordInputSettings( new_algo=pwd.new_algo, new_password_hash=new_password_hash, hint=hint, email=email, new_secure_settings=None ) )) except errors.EmailUnconfirmedError as e: code = email_code_callback(e.code_length) if inspect.isawaitable(code): code = await code code = str(code) await self(functions.account.ConfirmPasswordEmailRequest(code)) return True # endregion # region with blocks async def __aenter__(self): return await self.start() async def __aexit__(self, *args): await self.disconnect() __enter__ = helpers._sync_enter __exit__ = helpers._sync_exit # endregion Telethon-1.39.0/telethon/client/bots.py000066400000000000000000000046251475566265000200360ustar00rootroot00000000000000import typing from .. import hints from ..tl import types, functions, custom if typing.TYPE_CHECKING: from .telegramclient import TelegramClient class BotMethods: async def inline_query( self: 'TelegramClient', bot: 'hints.EntityLike', query: str, *, entity: 'hints.EntityLike' = None, offset: str = None, geo_point: 'types.GeoPoint' = None) -> custom.InlineResults: """ Makes an inline query to the specified bot (``@vote New Poll``). Arguments bot (`entity`): The bot entity to which the inline query should be made. query (`str`): The query that should be made to the bot. entity (`entity`, optional): The entity where the inline query is being made from. Certain bots use this to display different results depending on where it's used, such as private chats, groups or channels. If specified, it will also be the default entity where the message will be sent after clicked. Otherwise, the "empty peer" will be used, which some bots may not handle correctly. offset (`str`, optional): The string offset to use for the bot. geo_point (:tl:`GeoPoint`, optional) The geo point location information to send to the bot for localised results. Available under some bots. Returns A list of `custom.InlineResult `. Example .. code-block:: python # Make an inline query to @like results = await client.inline_query('like', 'Do you like Telethon?') # Send the first result to some chat message = await results[0].click('TelethonOffTopic') """ bot = await self.get_input_entity(bot) if entity: peer = await self.get_input_entity(entity) else: peer = types.InputPeerEmpty() result = await self(functions.messages.GetInlineBotResultsRequest( bot=bot, peer=peer, query=query, offset=offset or '', geo_point=geo_point )) return custom.InlineResults(self, result, entity=peer if entity else None) Telethon-1.39.0/telethon/client/buttons.py000066400000000000000000000063201475566265000205570ustar00rootroot00000000000000import typing from .. import utils, hints from ..tl import types, custom class ButtonMethods: @staticmethod def build_reply_markup( buttons: 'typing.Optional[hints.MarkupLike]', inline_only: bool = False) -> 'typing.Optional[types.TypeReplyMarkup]': """ Builds a :tl:`ReplyInlineMarkup` or :tl:`ReplyKeyboardMarkup` for the given buttons. Does nothing if either no buttons are provided or the provided argument is already a reply markup. You should consider using this method if you are going to reuse the markup very often. Otherwise, it is not necessary. This method is **not** asynchronous (don't use ``await`` on it). Arguments buttons (`hints.MarkupLike`): The button, list of buttons, array of buttons or markup to convert into a markup. inline_only (`bool`, optional): Whether the buttons **must** be inline buttons only or not. Example .. code-block:: python from telethon import Button markup = client.build_reply_markup(Button.inline('hi')) # later await client.send_message(chat, 'click me', buttons=markup) """ if buttons is None: return None try: if buttons.SUBCLASS_OF_ID == 0xe2e10ef2: return buttons # crc32(b'ReplyMarkup'): except AttributeError: pass if not utils.is_list_like(buttons): buttons = [[buttons]] elif not buttons or not utils.is_list_like(buttons[0]): buttons = [buttons] is_inline = False is_normal = False resize = None single_use = None selective = None rows = [] for row in buttons: current = [] for button in row: if isinstance(button, custom.Button): if button.resize is not None: resize = button.resize if button.single_use is not None: single_use = button.single_use if button.selective is not None: selective = button.selective button = button.button elif isinstance(button, custom.MessageButton): button = button.button inline = custom.Button._is_inline(button) is_inline |= inline is_normal |= not inline if button.SUBCLASS_OF_ID == 0xbad74a3: # 0xbad74a3 == crc32(b'KeyboardButton') current.append(button) if current: rows.append(types.KeyboardButtonRow(current)) if inline_only and is_normal: raise ValueError('You cannot use non-inline buttons here') elif is_inline == is_normal and is_normal: raise ValueError('You cannot mix inline with normal buttons') elif is_inline: return types.ReplyInlineMarkup(rows) # elif is_normal: return types.ReplyKeyboardMarkup( rows, resize=resize, single_use=single_use, selective=selective) Telethon-1.39.0/telethon/client/chats.py000066400000000000000000001441711475566265000201720ustar00rootroot00000000000000import asyncio import inspect import itertools import string import typing from .. import helpers, utils, hints, errors from ..requestiter import RequestIter from ..tl import types, functions, custom if typing.TYPE_CHECKING: from .telegramclient import TelegramClient _MAX_PARTICIPANTS_CHUNK_SIZE = 200 _MAX_ADMIN_LOG_CHUNK_SIZE = 100 _MAX_PROFILE_PHOTO_CHUNK_SIZE = 100 class _ChatAction: _str_mapping = { 'typing': types.SendMessageTypingAction(), 'contact': types.SendMessageChooseContactAction(), 'game': types.SendMessageGamePlayAction(), 'location': types.SendMessageGeoLocationAction(), 'sticker': types.SendMessageChooseStickerAction(), 'record-audio': types.SendMessageRecordAudioAction(), 'record-voice': types.SendMessageRecordAudioAction(), # alias 'record-round': types.SendMessageRecordRoundAction(), 'record-video': types.SendMessageRecordVideoAction(), 'audio': types.SendMessageUploadAudioAction(1), 'voice': types.SendMessageUploadAudioAction(1), # alias 'song': types.SendMessageUploadAudioAction(1), # alias 'round': types.SendMessageUploadRoundAction(1), 'video': types.SendMessageUploadVideoAction(1), 'photo': types.SendMessageUploadPhotoAction(1), 'document': types.SendMessageUploadDocumentAction(1), 'file': types.SendMessageUploadDocumentAction(1), # alias 'cancel': types.SendMessageCancelAction() } def __init__(self, client, chat, action, *, delay, auto_cancel): self._client = client self._chat = chat self._action = action self._delay = delay self._auto_cancel = auto_cancel self._request = None self._task = None self._running = False async def __aenter__(self): self._chat = await self._client.get_input_entity(self._chat) # Since `self._action` is passed by reference we can avoid # recreating the request all the time and still modify # `self._action.progress` directly in `progress`. self._request = functions.messages.SetTypingRequest( self._chat, self._action) self._running = True self._task = self._client.loop.create_task(self._update()) return self async def __aexit__(self, *args): self._running = False if self._task: self._task.cancel() try: await self._task except asyncio.CancelledError: pass self._task = None __enter__ = helpers._sync_enter __exit__ = helpers._sync_exit async def _update(self): try: while self._running: await self._client(self._request) await asyncio.sleep(self._delay) except ConnectionError: pass except asyncio.CancelledError: if self._auto_cancel: await self._client(functions.messages.SetTypingRequest( self._chat, types.SendMessageCancelAction())) def progress(self, current, total): if hasattr(self._action, 'progress'): self._action.progress = 100 * round(current / total) class _ParticipantsIter(RequestIter): async def _init(self, entity, filter, search): if isinstance(filter, type): if filter in (types.ChannelParticipantsBanned, types.ChannelParticipantsKicked, types.ChannelParticipantsSearch, types.ChannelParticipantsContacts): # These require a `q` parameter (support types for convenience) filter = filter('') else: filter = filter() entity = await self.client.get_input_entity(entity) ty = helpers._entity_type(entity) if search and (filter or ty != helpers._EntityType.CHANNEL): # We need to 'search' ourselves unless we have a PeerChannel search = search.casefold() self.filter_entity = lambda ent: ( search in utils.get_display_name(ent).casefold() or search in (getattr(ent, 'username', None) or '').casefold() ) else: self.filter_entity = lambda ent: True # Only used for channels, but we should always set the attribute # Called `requests` even though it's just one for legacy purposes. self.requests = None if ty == helpers._EntityType.CHANNEL: if self.limit <= 0: # May not have access to the channel, but getFull can get the .total. self.total = (await self.client( functions.channels.GetFullChannelRequest(entity) )).full_chat.participants_count raise StopAsyncIteration self.seen = set() self.requests = functions.channels.GetParticipantsRequest( channel=entity, filter=filter or types.ChannelParticipantsSearch(search), offset=0, limit=_MAX_PARTICIPANTS_CHUNK_SIZE, hash=0 ) elif ty == helpers._EntityType.CHAT: full = await self.client( functions.messages.GetFullChatRequest(entity.chat_id)) if not isinstance( full.full_chat.participants, types.ChatParticipants): # ChatParticipantsForbidden won't have ``.participants`` self.total = 0 raise StopAsyncIteration self.total = len(full.full_chat.participants.participants) users = {user.id: user for user in full.users} for participant in full.full_chat.participants.participants: if isinstance(participant, types.ChannelParticipantLeft): # See issue #3231 to learn why this is ignored. continue elif isinstance(participant, types.ChannelParticipantBanned): user_id = participant.peer.user_id else: user_id = participant.user_id user = users[user_id] if not self.filter_entity(user): continue user = users[user_id] user.participant = participant self.buffer.append(user) return True else: self.total = 1 if self.limit != 0: user = await self.client.get_entity(entity) if self.filter_entity(user): user.participant = None self.buffer.append(user) return True async def _load_next_chunk(self): if not self.requests: return True self.requests.limit = min(self.limit - self.requests.offset, _MAX_PARTICIPANTS_CHUNK_SIZE) if self.requests.offset > self.limit: return True if self.total is None: f = self.requests.filter if ( not isinstance(f, types.ChannelParticipantsRecent) and (not isinstance(f, types.ChannelParticipantsSearch) or f.q) ): # Only do an additional getParticipants here to get the total # if there's a filter which would reduce the real total number. # getParticipants is cheaper than getFull. self.total = (await self.client(functions.channels.GetParticipantsRequest( channel=self.requests.channel, filter=types.ChannelParticipantsRecent(), offset=0, limit=1, hash=0 ))).count participants = await self.client(self.requests) if self.total is None: # Will only get here if there was one request with a filter that matched all users. self.total = participants.count if not participants.users: self.requests = None return self.requests.offset += len(participants.participants) users = {user.id: user for user in participants.users} for participant in participants.participants: if isinstance(participant, types.ChannelParticipantBanned): if not isinstance(participant.peer, types.PeerUser): # May have the entire channel banned. See #3105. continue user_id = participant.peer.user_id else: user_id = participant.user_id user = users[user_id] if not self.filter_entity(user) or user.id in self.seen: continue self.seen.add(user_id) user = users[user_id] user.participant = participant self.buffer.append(user) class _AdminLogIter(RequestIter): async def _init( self, entity, admins, search, min_id, max_id, join, leave, invite, restrict, unrestrict, ban, unban, promote, demote, info, settings, pinned, edit, delete, group_call ): if any((join, leave, invite, restrict, unrestrict, ban, unban, promote, demote, info, settings, pinned, edit, delete, group_call)): events_filter = types.ChannelAdminLogEventsFilter( join=join, leave=leave, invite=invite, ban=restrict, unban=unrestrict, kick=ban, unkick=unban, promote=promote, demote=demote, info=info, settings=settings, pinned=pinned, edit=edit, delete=delete, group_call=group_call ) else: events_filter = None self.entity = await self.client.get_input_entity(entity) admin_list = [] if admins: if not utils.is_list_like(admins): admins = (admins,) for admin in admins: admin_list.append(await self.client.get_input_entity(admin)) self.request = functions.channels.GetAdminLogRequest( self.entity, q=search or '', min_id=min_id, max_id=max_id, limit=0, events_filter=events_filter, admins=admin_list or None ) async def _load_next_chunk(self): self.request.limit = min(self.left, _MAX_ADMIN_LOG_CHUNK_SIZE) r = await self.client(self.request) entities = {utils.get_peer_id(x): x for x in itertools.chain(r.users, r.chats)} self.request.max_id = min((e.id for e in r.events), default=0) for ev in r.events: if isinstance(ev.action, types.ChannelAdminLogEventActionEditMessage): ev.action.prev_message._finish_init( self.client, entities, self.entity) ev.action.new_message._finish_init( self.client, entities, self.entity) elif isinstance(ev.action, types.ChannelAdminLogEventActionDeleteMessage): ev.action.message._finish_init( self.client, entities, self.entity) self.buffer.append(custom.AdminLogEvent(ev, entities)) if len(r.events) < self.request.limit: return True class _ProfilePhotoIter(RequestIter): async def _init( self, entity, offset, max_id ): entity = await self.client.get_input_entity(entity) ty = helpers._entity_type(entity) if ty == helpers._EntityType.USER: self.request = functions.photos.GetUserPhotosRequest( entity, offset=offset, max_id=max_id, limit=1 ) else: self.request = functions.messages.SearchRequest( peer=entity, q='', filter=types.InputMessagesFilterChatPhotos(), min_date=None, max_date=None, offset_id=0, add_offset=offset, limit=1, max_id=max_id, min_id=0, hash=0 ) if self.limit == 0: self.request.limit = 1 result = await self.client(self.request) if isinstance(result, types.photos.Photos): self.total = len(result.photos) elif isinstance(result, types.messages.Messages): self.total = len(result.messages) else: # Luckily both photosSlice and messages have a count for total self.total = getattr(result, 'count', None) async def _load_next_chunk(self): self.request.limit = min(self.left, _MAX_PROFILE_PHOTO_CHUNK_SIZE) result = await self.client(self.request) if isinstance(result, types.photos.Photos): self.buffer = result.photos self.left = len(self.buffer) self.total = len(self.buffer) elif isinstance(result, types.messages.Messages): self.buffer = [x.action.photo for x in result.messages if isinstance(x.action, types.MessageActionChatEditPhoto)] self.left = len(self.buffer) self.total = len(self.buffer) elif isinstance(result, types.photos.PhotosSlice): self.buffer = result.photos self.total = result.count if len(self.buffer) < self.request.limit: self.left = len(self.buffer) else: self.request.offset += len(result.photos) else: # Some broadcast channels have a photo that this request doesn't # retrieve for whatever random reason the Telegram server feels. # # This means the `total` count may be wrong but there's not much # that can be done around it (perhaps there are too many photos # and this is only a partial result so it's not possible to just # use the len of the result). self.total = getattr(result, 'count', None) # Unconditionally fetch the full channel to obtain this photo and # yield it with the rest (unless it's a duplicate). seen_id = None if isinstance(result, types.messages.ChannelMessages): channel = await self.client(functions.channels.GetFullChannelRequest(self.request.peer)) photo = channel.full_chat.chat_photo if isinstance(photo, types.Photo): self.buffer.append(photo) seen_id = photo.id self.buffer.extend( x.action.photo for x in result.messages if isinstance(x.action, types.MessageActionChatEditPhoto) and x.action.photo.id != seen_id ) if len(result.messages) < self.request.limit: self.left = len(self.buffer) elif result.messages: self.request.add_offset = 0 self.request.offset_id = result.messages[-1].id class ChatMethods: # region Public methods def iter_participants( self: 'TelegramClient', entity: 'hints.EntityLike', limit: float = None, *, search: str = '', filter: 'types.TypeChannelParticipantsFilter' = None, aggressive: bool = False) -> _ParticipantsIter: """ Iterator over the participants belonging to the specified chat. The order is unspecified. Arguments entity (`entity`): The entity from which to retrieve the participants list. limit (`int`): Limits amount of participants fetched. search (`str`, optional): Look for participants with this string in name/username. filter (:tl:`ChannelParticipantsFilter`, optional): The filter to be used, if you want e.g. only admins Note that you might not have permissions for some filter. This has no effect for normal chats or users. .. note:: The filter :tl:`ChannelParticipantsBanned` will return *restricted* users. If you want *banned* users you should use :tl:`ChannelParticipantsKicked` instead. aggressive (`bool`, optional): Does nothing. This is kept for backwards-compatibility. There have been several changes to Telegram's API that limits the amount of members that can be retrieved, and this was a hack that no longer works. Yields The :tl:`User` objects returned by :tl:`GetParticipantsRequest` with an additional ``.participant`` attribute which is the matched :tl:`ChannelParticipant` type for channels/megagroups or :tl:`ChatParticipants` for normal chats. Example .. code-block:: python # Show all user IDs in a chat async for user in client.iter_participants(chat): print(user.id) # Search by name async for user in client.iter_participants(chat, search='name'): print(user.username) # Filter by admins from telethon.tl.types import ChannelParticipantsAdmins async for user in client.iter_participants(chat, filter=ChannelParticipantsAdmins): print(user.first_name) """ return _ParticipantsIter( self, limit, entity=entity, filter=filter, search=search ) async def get_participants( self: 'TelegramClient', *args, **kwargs) -> 'hints.TotalList': """ Same as `iter_participants()`, but returns a `TotalList ` instead. Example .. code-block:: python users = await client.get_participants(chat) print(users[0].first_name) for user in users: if user.username is not None: print(user.username) """ return await self.iter_participants(*args, **kwargs).collect() get_participants.__signature__ = inspect.signature(iter_participants) def iter_admin_log( self: 'TelegramClient', entity: 'hints.EntityLike', limit: float = None, *, max_id: int = 0, min_id: int = 0, search: str = None, admins: 'hints.EntitiesLike' = None, join: bool = None, leave: bool = None, invite: bool = None, restrict: bool = None, unrestrict: bool = None, ban: bool = None, unban: bool = None, promote: bool = None, demote: bool = None, info: bool = None, settings: bool = None, pinned: bool = None, edit: bool = None, delete: bool = None, group_call: bool = None) -> _AdminLogIter: """ Iterator over the admin log for the specified channel. The default order is from the most recent event to to the oldest. Note that you must be an administrator of it to use this method. If none of the filters are present (i.e. they all are `None`), *all* event types will be returned. If at least one of them is `True`, only those that are true will be returned. Arguments entity (`entity`): The channel entity from which to get its admin log. limit (`int` | `None`, optional): Number of events to be retrieved. The limit may also be `None`, which would eventually return the whole history. max_id (`int`): All the events with a higher (newer) ID or equal to this will be excluded. min_id (`int`): All the events with a lower (older) ID or equal to this will be excluded. search (`str`): The string to be used as a search query. admins (`entity` | `list`): If present, the events will be filtered by these admins (or single admin) and only those caused by them will be returned. join (`bool`): If `True`, events for when a user joined will be returned. leave (`bool`): If `True`, events for when a user leaves will be returned. invite (`bool`): If `True`, events for when a user joins through an invite link will be returned. restrict (`bool`): If `True`, events with partial restrictions will be returned. This is what the API calls "ban". unrestrict (`bool`): If `True`, events removing restrictions will be returned. This is what the API calls "unban". ban (`bool`): If `True`, events applying or removing all restrictions will be returned. This is what the API calls "kick" (restricting all permissions removed is a ban, which kicks the user). unban (`bool`): If `True`, events removing all restrictions will be returned. This is what the API calls "unkick". promote (`bool`): If `True`, events with admin promotions will be returned. demote (`bool`): If `True`, events with admin demotions will be returned. info (`bool`): If `True`, events changing the group info will be returned. settings (`bool`): If `True`, events changing the group settings will be returned. pinned (`bool`): If `True`, events of new pinned messages will be returned. edit (`bool`): If `True`, events of message edits will be returned. delete (`bool`): If `True`, events of message deletions will be returned. group_call (`bool`): If `True`, events related to group calls will be returned. Yields Instances of `AdminLogEvent `. Example .. code-block:: python async for event in client.iter_admin_log(channel): if event.changed_title: print('The title changed from', event.old, 'to', event.new) """ return _AdminLogIter( self, limit, entity=entity, admins=admins, search=search, min_id=min_id, max_id=max_id, join=join, leave=leave, invite=invite, restrict=restrict, unrestrict=unrestrict, ban=ban, unban=unban, promote=promote, demote=demote, info=info, settings=settings, pinned=pinned, edit=edit, delete=delete, group_call=group_call ) async def get_admin_log( self: 'TelegramClient', *args, **kwargs) -> 'hints.TotalList': """ Same as `iter_admin_log()`, but returns a ``list`` instead. Example .. code-block:: python # Get a list of deleted message events which said "heck" events = await client.get_admin_log(channel, search='heck', delete=True) # Print the old message before it was deleted print(events[0].old) """ return await self.iter_admin_log(*args, **kwargs).collect() get_admin_log.__signature__ = inspect.signature(iter_admin_log) def iter_profile_photos( self: 'TelegramClient', entity: 'hints.EntityLike', limit: int = None, *, offset: int = 0, max_id: int = 0) -> _ProfilePhotoIter: """ Iterator over a user's profile photos or a chat's photos. The order is from the most recent photo to the oldest. Arguments entity (`entity`): The entity from which to get the profile or chat photos. limit (`int` | `None`, optional): Number of photos to be retrieved. The limit may also be `None`, which would eventually all the photos that are still available. offset (`int`): How many photos should be skipped before returning the first one. max_id (`int`): The maximum ID allowed when fetching photos. Yields Instances of :tl:`Photo`. Example .. code-block:: python # Download all the profile photos of some user async for photo in client.iter_profile_photos(user): await client.download_media(photo) """ return _ProfilePhotoIter( self, limit, entity=entity, offset=offset, max_id=max_id ) async def get_profile_photos( self: 'TelegramClient', *args, **kwargs) -> 'hints.TotalList': """ Same as `iter_profile_photos()`, but returns a `TotalList ` instead. Example .. code-block:: python # Get the photos of a channel photos = await client.get_profile_photos(channel) # Download the oldest photo await client.download_media(photos[-1]) """ return await self.iter_profile_photos(*args, **kwargs).collect() get_profile_photos.__signature__ = inspect.signature(iter_profile_photos) def action( self: 'TelegramClient', entity: 'hints.EntityLike', action: 'typing.Union[str, types.TypeSendMessageAction]', *, delay: float = 4, auto_cancel: bool = True) -> 'typing.Union[_ChatAction, typing.Coroutine]': """ Returns a context-manager object to represent a "chat action". Chat actions indicate things like "user is typing", "user is uploading a photo", etc. If the action is ``'cancel'``, you should just ``await`` the result, since it makes no sense to use a context-manager for it. See the example below for intended usage. Arguments entity (`entity`): The entity where the action should be showed in. action (`str` | :tl:`SendMessageAction`): The action to show. You can either pass a instance of :tl:`SendMessageAction` or better, a string used while: * ``'typing'``: typing a text message. * ``'contact'``: choosing a contact. * ``'game'``: playing a game. * ``'location'``: choosing a geo location. * ``'sticker'``: choosing a sticker. * ``'record-audio'``: recording a voice note. You may use ``'record-voice'`` as alias. * ``'record-round'``: recording a round video. * ``'record-video'``: recording a normal video. * ``'audio'``: sending an audio file (voice note or song). You may use ``'voice'`` and ``'song'`` as aliases. * ``'round'``: uploading a round video. * ``'video'``: uploading a video file. * ``'photo'``: uploading a photo. * ``'document'``: uploading a document file. You may use ``'file'`` as alias. * ``'cancel'``: cancel any pending action in this chat. Invalid strings will raise a ``ValueError``. delay (`int` | `float`): The delay, in seconds, to wait between sending actions. For example, if the delay is 5 and it takes 7 seconds to do something, three requests will be made at 0s, 5s, and 7s to cancel the action. auto_cancel (`bool`): Whether the action should be cancelled once the context manager exists or not. The default is `True`, since you don't want progress to be shown when it has already completed. Returns Either a context-manager object or a coroutine. Example .. code-block:: python # Type for 2 seconds, then send a message async with client.action(chat, 'typing'): await asyncio.sleep(2) await client.send_message(chat, 'Hello world! I type slow ^^') # Cancel any previous action await client.action(chat, 'cancel') # Upload a document, showing its progress (most clients ignore this) async with client.action(chat, 'document') as action: await client.send_file(chat, zip_file, progress_callback=action.progress) """ if isinstance(action, str): try: action = _ChatAction._str_mapping[action.lower()] except KeyError: raise ValueError( 'No such action "{}"'.format(action)) from None elif not isinstance(action, types.TLObject) or action.SUBCLASS_OF_ID != 0x20b2cc21: # 0x20b2cc21 = crc32(b'SendMessageAction') if isinstance(action, type): raise ValueError('You must pass an instance, not the class') else: raise ValueError('Cannot use {} as action'.format(action)) if isinstance(action, types.SendMessageCancelAction): # ``SetTypingRequest.resolve`` will get input peer of ``entity``. return self(functions.messages.SetTypingRequest( entity, types.SendMessageCancelAction())) return _ChatAction( self, entity, action, delay=delay, auto_cancel=auto_cancel) async def edit_admin( self: 'TelegramClient', entity: 'hints.EntityLike', user: 'hints.EntityLike', *, change_info: bool = None, post_messages: bool = None, edit_messages: bool = None, delete_messages: bool = None, ban_users: bool = None, invite_users: bool = None, pin_messages: bool = None, add_admins: bool = None, manage_call: bool = None, anonymous: bool = None, is_admin: bool = None, title: str = None) -> types.Updates: """ Edits admin permissions for someone in a chat. Raises an error if a wrong combination of rights are given (e.g. you don't have enough permissions to grant one). Unless otherwise stated, permissions will work in channels and megagroups. Arguments entity (`entity`): The channel, megagroup or chat where the promotion should happen. user (`entity`): The user to be promoted. change_info (`bool`, optional): Whether the user will be able to change info. post_messages (`bool`, optional): Whether the user will be able to post in the channel. This will only work in broadcast channels. edit_messages (`bool`, optional): Whether the user will be able to edit messages in the channel. This will only work in broadcast channels. delete_messages (`bool`, optional): Whether the user will be able to delete messages. ban_users (`bool`, optional): Whether the user will be able to ban users. invite_users (`bool`, optional): Whether the user will be able to invite users. Needs some testing. pin_messages (`bool`, optional): Whether the user will be able to pin messages. add_admins (`bool`, optional): Whether the user will be able to add admins. manage_call (`bool`, optional): Whether the user will be able to manage group calls. anonymous (`bool`, optional): Whether the user will remain anonymous when sending messages. The sender of the anonymous messages becomes the group itself. .. note:: Users may be able to identify the anonymous admin by its custom title, so additional care is needed when using both ``anonymous`` and custom titles. For example, if multiple anonymous admins share the same title, users won't be able to distinguish them. is_admin (`bool`, optional): Whether the user will be an admin in the chat. This will only work in small group chats. Whether the user will be an admin in the chat. This is the only permission available in small group chats, and when used in megagroups, all non-explicitly set permissions will have this value. Essentially, only passing ``is_admin=True`` will grant all permissions, but you can still disable those you need. title (`str`, optional): The custom title (also known as "rank") to show for this admin. This text will be shown instead of the "admin" badge. This will only work in channels and megagroups. When left unspecified or empty, the default localized "admin" badge will be shown. Returns The resulting :tl:`Updates` object. Example .. code-block:: python # Allowing `user` to pin messages in `chat` await client.edit_admin(chat, user, pin_messages=True) # Granting all permissions except for `add_admins` await client.edit_admin(chat, user, is_admin=True, add_admins=False) """ entity = await self.get_input_entity(entity) user = await self.get_input_entity(user) perm_names = ( 'change_info', 'post_messages', 'edit_messages', 'delete_messages', 'ban_users', 'invite_users', 'pin_messages', 'add_admins', 'anonymous', 'manage_call', ) ty = helpers._entity_type(entity) if ty == helpers._EntityType.CHANNEL: # If we try to set these permissions in a megagroup, we # would get a RIGHT_FORBIDDEN. However, it makes sense # that an admin can post messages, so we want to avoid the error if post_messages or edit_messages: # TODO get rid of this once sessions cache this information if entity.channel_id not in self._megagroup_cache: full_entity = await self.get_entity(entity) self._megagroup_cache[entity.channel_id] = full_entity.megagroup if self._megagroup_cache[entity.channel_id]: post_messages = None edit_messages = None perms = locals() return await self(functions.channels.EditAdminRequest(entity, user, types.ChatAdminRights(**{ # A permission is its explicit (not-None) value or `is_admin`. # This essentially makes `is_admin` be the default value. name: perms[name] if perms[name] is not None else is_admin for name in perm_names }), rank=title or '')) elif ty == helpers._EntityType.CHAT: # If the user passed any permission in a small # group chat, they must be a full admin to have it. if is_admin is None: is_admin = any(locals()[x] for x in perm_names) return await self(functions.messages.EditChatAdminRequest( entity.chat_id, user, is_admin=is_admin)) else: raise ValueError( 'You can only edit permissions in groups and channels') async def edit_permissions( self: 'TelegramClient', entity: 'hints.EntityLike', user: 'typing.Optional[hints.EntityLike]' = None, until_date: 'hints.DateLike' = None, *, view_messages: bool = True, send_messages: bool = True, send_media: bool = True, send_stickers: bool = True, send_gifs: bool = True, send_games: bool = True, send_inline: bool = True, embed_link_previews: bool = True, send_polls: bool = True, change_info: bool = True, invite_users: bool = True, pin_messages: bool = True) -> types.Updates: """ Edits user restrictions in a chat. Set an argument to `False` to apply a restriction (i.e. remove the permission), or omit them to use the default `True` (i.e. don't apply a restriction). Raises an error if a wrong combination of rights are given (e.g. you don't have enough permissions to revoke one). By default, each boolean argument is `True`, meaning that it is true that the user has access to the default permission and may be able to make use of it. If you set an argument to `False`, then a restriction is applied regardless of the default permissions. It is important to note that `True` does *not* mean grant, only "don't restrict", and this is where the default permissions come in. A user may have not been revoked the ``pin_messages`` permission (it is `True`) but they won't be able to use it if the default permissions don't allow it either. Arguments entity (`entity`): The channel or megagroup where the restriction should happen. user (`entity`, optional): If specified, the permission will be changed for the specific user. If left as `None`, the default chat permissions will be updated. until_date (`DateLike`, optional): When the user will be unbanned. If the due date or duration is longer than 366 days or shorter than 30 seconds, the ban will be forever. Defaults to ``0`` (ban forever). view_messages (`bool`, optional): Whether the user is able to view messages or not. Forbidding someone from viewing messages equals to banning them. This will only work if ``user`` is set. send_messages (`bool`, optional): Whether the user is able to send messages or not. send_media (`bool`, optional): Whether the user is able to send media or not. send_stickers (`bool`, optional): Whether the user is able to send stickers or not. send_gifs (`bool`, optional): Whether the user is able to send animated gifs or not. send_games (`bool`, optional): Whether the user is able to send games or not. send_inline (`bool`, optional): Whether the user is able to use inline bots or not. embed_link_previews (`bool`, optional): Whether the user is able to enable the link preview in the messages they send. Note that the user will still be able to send messages with links if this permission is removed, but these links won't display a link preview. send_polls (`bool`, optional): Whether the user is able to send polls or not. change_info (`bool`, optional): Whether the user is able to change info or not. invite_users (`bool`, optional): Whether the user is able to invite other users or not. pin_messages (`bool`, optional): Whether the user is able to pin messages or not. Returns The resulting :tl:`Updates` object. Example .. code-block:: python from datetime import timedelta # Banning `user` from `chat` for 1 minute await client.edit_permissions(chat, user, timedelta(minutes=1), view_messages=False) # Banning `user` from `chat` forever await client.edit_permissions(chat, user, view_messages=False) # Kicking someone (ban + un-ban) await client.edit_permissions(chat, user, view_messages=False) await client.edit_permissions(chat, user) """ entity = await self.get_input_entity(entity) ty = helpers._entity_type(entity) if ty != helpers._EntityType.CHANNEL: raise ValueError('You must pass either a channel or a supergroup') rights = types.ChatBannedRights( until_date=until_date, view_messages=not view_messages, send_messages=not send_messages, send_media=not send_media, send_stickers=not send_stickers, send_gifs=not send_gifs, send_games=not send_games, send_inline=not send_inline, embed_links=not embed_link_previews, send_polls=not send_polls, change_info=not change_info, invite_users=not invite_users, pin_messages=not pin_messages ) if user is None: return await self(functions.messages.EditChatDefaultBannedRightsRequest( peer=entity, banned_rights=rights )) user = await self.get_input_entity(user) return await self(functions.channels.EditBannedRequest( channel=entity, participant=user, banned_rights=rights )) async def kick_participant( self: 'TelegramClient', entity: 'hints.EntityLike', user: 'typing.Optional[hints.EntityLike]' ): """ Kicks a user from a chat. Kicking yourself (``'me'``) will result in leaving the chat. .. note:: Attempting to kick someone who was banned will remove their restrictions (and thus unbanning them), since kicking is just ban + unban. Arguments entity (`entity`): The channel or chat where the user should be kicked from. user (`entity`, optional): The user to kick. Returns Returns the service `Message ` produced about a user being kicked, if any. Example .. code-block:: python # Kick some user from some chat, and deleting the service message msg = await client.kick_participant(chat, user) await msg.delete() # Leaving chat await client.kick_participant(chat, 'me') """ entity = await self.get_input_entity(entity) user = await self.get_input_entity(user) ty = helpers._entity_type(entity) if ty == helpers._EntityType.CHAT: resp = await self(functions.messages.DeleteChatUserRequest(entity.chat_id, user)) elif ty == helpers._EntityType.CHANNEL: if isinstance(user, types.InputPeerSelf): # Despite no longer being in the channel, the account still # seems to get the service message. resp = await self(functions.channels.LeaveChannelRequest(entity)) else: resp = await self(functions.channels.EditBannedRequest( channel=entity, participant=user, banned_rights=types.ChatBannedRights( until_date=None, view_messages=True) )) await asyncio.sleep(0.5) await self(functions.channels.EditBannedRequest( channel=entity, participant=user, banned_rights=types.ChatBannedRights(until_date=None) )) else: raise ValueError('You must pass either a channel or a chat') return self._get_response_message(None, resp, entity) async def get_permissions( self: 'TelegramClient', entity: 'hints.EntityLike', user: 'hints.EntityLike' = None ) -> 'typing.Optional[custom.ParticipantPermissions]': """ Fetches the permissions of a user in a specific chat or channel or get Default Restricted Rights of Chat or Channel. .. note:: This request has to fetch the entire chat for small group chats, which can get somewhat expensive, so use of a cache is advised. Arguments entity (`entity`): The channel or chat the user is participant of. user (`entity`, optional): Target user. Returns A `ParticipantPermissions ` instance. Refer to its documentation to see what properties are available. Example .. code-block:: python permissions = await client.get_permissions(chat, user) if permissions.is_admin: # do something # Get Banned Permissions of Chat await client.get_permissions(chat) """ entity = await self.get_entity(entity) if not user: if isinstance(entity, types.Channel): FullChat = await self(functions.channels.GetFullChannelRequest(entity)) elif isinstance(entity, types.Chat): FullChat = await self(functions.messages.GetFullChatRequest(entity.id)) else: return return FullChat.chats[0].default_banned_rights entity = await self.get_input_entity(entity) user = await self.get_input_entity(user) if helpers._entity_type(entity) == helpers._EntityType.CHANNEL: participant = await self(functions.channels.GetParticipantRequest( entity, user )) return custom.ParticipantPermissions(participant.participant, False) elif helpers._entity_type(entity) == helpers._EntityType.CHAT: chat = await self(functions.messages.GetFullChatRequest( entity.chat_id )) if isinstance(user, types.InputPeerSelf): user = await self.get_me(input_peer=True) for participant in chat.full_chat.participants.participants: if participant.user_id == user.user_id: return custom.ParticipantPermissions(participant, True) raise errors.UserNotParticipantError(None) raise ValueError('You must pass either a channel or a chat') async def get_stats( self: 'TelegramClient', entity: 'hints.EntityLike', message: 'typing.Union[int, types.Message]' = None, ): """ Retrieves statistics from the given megagroup or broadcast channel. Note that some restrictions apply before being able to fetch statistics, in particular the channel must have enough members (for megagroups, this requires `at least 500 members`_). Arguments entity (`entity`): The channel from which to get statistics. message (`int` | ``Message``, optional): The message ID from which to get statistics, if your goal is to obtain the statistics of a single message. Raises If the given entity is not a channel (broadcast or megagroup), a `TypeError` is raised. If there are not enough members (poorly named) errors such as ``telethon.errors.ChatAdminRequiredError`` will appear. Returns If both ``entity`` and ``message`` were provided, returns :tl:`MessageStats`. Otherwise, either :tl:`BroadcastStats` or :tl:`MegagroupStats`, depending on whether the input belonged to a broadcast channel or megagroup. Example .. code-block:: python # Some megagroup or channel username or ID to fetch channel = -100123 stats = await client.get_stats(channel) print('Stats from', stats.period.min_date, 'to', stats.period.max_date, ':') print(stats.stringify()) .. _`at least 500 members`: https://telegram.org/blog/profile-videos-people-nearby-and-more """ entity = await self.get_input_entity(entity) if helpers._entity_type(entity) != helpers._EntityType.CHANNEL: raise TypeError('You must pass a channel entity') message = utils.get_message_id(message) if message is not None: try: req = functions.stats.GetMessageStatsRequest(entity, message) return await self(req) except errors.StatsMigrateError as e: dc = e.dc else: # Don't bother fetching the Channel entity (costs a request), instead # try to guess and if it fails we know it's the other one (best case # no extra request, worst just one). try: req = functions.stats.GetBroadcastStatsRequest(entity) return await self(req) except errors.StatsMigrateError as e: dc = e.dc except errors.BroadcastRequiredError: req = functions.stats.GetMegagroupStatsRequest(entity) try: return await self(req) except errors.StatsMigrateError as e: dc = e.dc sender = await self._borrow_exported_sender(dc) try: # req will be resolved to use the right types inside by now return await sender.send(req) finally: await self._return_exported_sender(sender) # endregion Telethon-1.39.0/telethon/client/dialogs.py000066400000000000000000000547401475566265000205140ustar00rootroot00000000000000import asyncio import inspect import itertools import typing from .. import helpers, utils, hints, errors from ..requestiter import RequestIter from ..tl import types, functions, custom _MAX_CHUNK_SIZE = 100 if typing.TYPE_CHECKING: from .telegramclient import TelegramClient def _dialog_message_key(peer, message_id): """ Get the key to get messages from a dialog. We cannot just use the message ID because channels share message IDs, and the peer ID is required to distinguish between them. But it is not necessary in small group chats and private chats. """ return (peer.channel_id if isinstance(peer, types.PeerChannel) else None), message_id class _DialogsIter(RequestIter): async def _init( self, offset_date, offset_id, offset_peer, ignore_pinned, ignore_migrated, folder ): self.request = functions.messages.GetDialogsRequest( offset_date=offset_date, offset_id=offset_id, offset_peer=offset_peer, limit=1, hash=0, exclude_pinned=ignore_pinned, folder_id=folder ) if self.limit <= 0: # Special case, get a single dialog and determine count dialogs = await self.client(self.request) self.total = getattr(dialogs, 'count', len(dialogs.dialogs)) raise StopAsyncIteration self.seen = set() self.offset_date = offset_date self.ignore_migrated = ignore_migrated async def _load_next_chunk(self): self.request.limit = min(self.left, _MAX_CHUNK_SIZE) r = await self.client(self.request) self.total = getattr(r, 'count', len(r.dialogs)) entities = {utils.get_peer_id(x): x for x in itertools.chain(r.users, r.chats) if not isinstance(x, (types.UserEmpty, types.ChatEmpty))} self.client._mb_entity_cache.extend(r.users, r.chats) messages = {} for m in r.messages: m._finish_init(self.client, entities, None) messages[_dialog_message_key(m.peer_id, m.id)] = m for d in r.dialogs: # We check the offset date here because Telegram may ignore it message = messages.get(_dialog_message_key(d.peer, d.top_message)) if self.offset_date: date = getattr(message, 'date', None) if not date or date.timestamp() > self.offset_date.timestamp(): continue peer_id = utils.get_peer_id(d.peer) if peer_id not in self.seen: self.seen.add(peer_id) if peer_id not in entities: # > In which case can a UserEmpty appear in the list of banned members? # > In a very rare cases. This is possible but isn't an expected behavior. # Real world example: https://t.me/TelethonChat/271471 continue cd = custom.Dialog(self.client, d, entities, message) if cd.dialog.pts: self.client._message_box.try_set_channel_state( utils.get_peer_id(d.peer, add_mark=False), cd.dialog.pts) if not self.ignore_migrated or getattr( cd.entity, 'migrated_to', None) is None: self.buffer.append(cd) if not self.buffer or len(r.dialogs) < self.request.limit\ or not isinstance(r, types.messages.DialogsSlice): # Buffer being empty means all returned dialogs were skipped (due to offsets). # Less than we requested means we reached the end, or # we didn't get a DialogsSlice which means we got all. return True # We can't use `messages[-1]` as the offset ID / date. # Why? Because pinned dialogs will mess with the order # in this list. Instead, we find the last dialog which # has a message, and use it as an offset. last_message = next(filter(None, ( messages.get(_dialog_message_key(d.peer, d.top_message)) for d in reversed(r.dialogs) )), None) self.request.exclude_pinned = True self.request.offset_id = last_message.id if last_message else 0 self.request.offset_date = last_message.date if last_message else None self.request.offset_peer = self.buffer[-1].input_entity class _DraftsIter(RequestIter): async def _init(self, entities, **kwargs): if not entities: r = await self.client(functions.messages.GetAllDraftsRequest()) items = r.updates else: peers = [] for entity in entities: peers.append(types.InputDialogPeer( await self.client.get_input_entity(entity))) r = await self.client(functions.messages.GetPeerDialogsRequest(peers)) items = r.dialogs # TODO Maybe there should be a helper method for this? entities = {utils.get_peer_id(x): x for x in itertools.chain(r.users, r.chats)} self.buffer.extend( custom.Draft(self.client, entities[utils.get_peer_id(d.peer)], d.draft) for d in items ) async def _load_next_chunk(self): return [] class DialogMethods: # region Public methods def iter_dialogs( self: 'TelegramClient', limit: float = None, *, offset_date: 'hints.DateLike' = None, offset_id: int = 0, offset_peer: 'hints.EntityLike' = types.InputPeerEmpty(), ignore_pinned: bool = False, ignore_migrated: bool = False, folder: int = None, archived: bool = None ) -> _DialogsIter: """ Iterator over the dialogs (open conversations/subscribed channels). The order is the same as the one seen in official applications (first pinned, them from those with the most recent message to those with the oldest message). Arguments limit (`int` | `None`): How many dialogs to be retrieved as maximum. Can be set to `None` to retrieve all dialogs. Note that this may take whole minutes if you have hundreds of dialogs, as Telegram will tell the library to slow down through a ``FloodWaitError``. offset_date (`datetime`, optional): The offset date to be used. offset_id (`int`, optional): The message ID to be used as an offset. offset_peer (:tl:`InputPeer`, optional): The peer to be used as an offset. ignore_pinned (`bool`, optional): Whether pinned dialogs should be ignored or not. When set to `True`, these won't be yielded at all. ignore_migrated (`bool`, optional): Whether :tl:`Chat` that have ``migrated_to`` a :tl:`Channel` should be included or not. By default all the chats in your dialogs are returned, but setting this to `True` will ignore (i.e. skip) them in the same way official applications do. folder (`int`, optional): The folder from which the dialogs should be retrieved. If left unspecified, all dialogs (including those from folders) will be returned. If set to ``0``, all dialogs that don't belong to any folder will be returned. If set to a folder number like ``1``, only those from said folder will be returned. By default Telegram assigns the folder ID ``1`` to archived chats, so you should use that if you need to fetch the archived dialogs. archived (`bool`, optional): Alias for `folder`. If unspecified, all will be returned, `False` implies ``folder=0`` and `True` implies ``folder=1``. Yields Instances of `Dialog `. Example .. code-block:: python # Print all dialog IDs and the title, nicely formatted async for dialog in client.iter_dialogs(): print('{:>14}: {}'.format(dialog.id, dialog.title)) """ if archived is not None: folder = 1 if archived else 0 return _DialogsIter( self, limit, offset_date=offset_date, offset_id=offset_id, offset_peer=offset_peer, ignore_pinned=ignore_pinned, ignore_migrated=ignore_migrated, folder=folder ) async def get_dialogs(self: 'TelegramClient', *args, **kwargs) -> 'hints.TotalList': """ Same as `iter_dialogs()`, but returns a `TotalList ` instead. Example .. code-block:: python # Get all open conversation, print the title of the first dialogs = await client.get_dialogs() first = dialogs[0] print(first.title) # Use the dialog somewhere else await client.send_message(first, 'hi') # Getting only non-archived dialogs (both equivalent) non_archived = await client.get_dialogs(folder=0) non_archived = await client.get_dialogs(archived=False) # Getting only archived dialogs (both equivalent) archived = await client.get_dialogs(folder=1) archived = await client.get_dialogs(archived=True) """ return await self.iter_dialogs(*args, **kwargs).collect() get_dialogs.__signature__ = inspect.signature(iter_dialogs) def iter_drafts( self: 'TelegramClient', entity: 'hints.EntitiesLike' = None ) -> _DraftsIter: """ Iterator over draft messages. The order is unspecified. Arguments entity (`hints.EntitiesLike`, optional): The entity or entities for which to fetch the draft messages. If left unspecified, all draft messages will be returned. Yields Instances of `Draft `. Example .. code-block:: python # Clear all drafts async for draft in client.get_drafts(): await draft.delete() # Getting the drafts with 'bot1' and 'bot2' async for draft in client.iter_drafts(['bot1', 'bot2']): print(draft.text) """ if entity and not utils.is_list_like(entity): entity = (entity,) # TODO Passing a limit here makes no sense return _DraftsIter(self, None, entities=entity) async def get_drafts( self: 'TelegramClient', entity: 'hints.EntitiesLike' = None ) -> 'hints.TotalList': """ Same as `iter_drafts()`, but returns a list instead. Example .. code-block:: python # Get drafts, print the text of the first drafts = await client.get_drafts() print(drafts[0].text) # Get the draft in your chat draft = await client.get_drafts('me') print(drafts.text) """ items = await self.iter_drafts(entity).collect() if not entity or utils.is_list_like(entity): return items else: return items[0] async def edit_folder( self: 'TelegramClient', entity: 'hints.EntitiesLike' = None, folder: typing.Union[int, typing.Sequence[int]] = None, *, unpack=None ) -> types.Updates: """ Edits the folder used by one or more dialogs to archive them. Arguments entity (entities): The entity or list of entities to move to the desired archive folder. folder (`int`): The folder to which the dialog should be archived to. If you want to "archive" a dialog, use ``folder=1``. If you want to "un-archive" it, use ``folder=0``. You may also pass a list with the same length as `entities` if you want to control where each entity will go. unpack (`int`, optional): If you want to unpack an archived folder, set this parameter to the folder number that you want to delete. When you unpack a folder, all the dialogs inside are moved to the folder number 0. You can only use this parameter if the other two are not set. Returns The :tl:`Updates` object that the request produces. Example .. code-block:: python # Archiving the first 5 dialogs dialogs = await client.get_dialogs(5) await client.edit_folder(dialogs, 1) # Un-archiving the third dialog (archiving to folder 0) await client.edit_folder(dialog[2], 0) # Moving the first dialog to folder 0 and the second to 1 dialogs = await client.get_dialogs(2) await client.edit_folder(dialogs, [0, 1]) # Un-archiving all dialogs await client.edit_folder(unpack=1) """ if (entity is None) == (unpack is None): raise ValueError('You can only set either entities or unpack, not both') if unpack is not None: return await self(functions.folders.DeleteFolderRequest( folder_id=unpack )) if not utils.is_list_like(entity): entities = [await self.get_input_entity(entity)] else: entities = await asyncio.gather( *(self.get_input_entity(x) for x in entity)) if folder is None: raise ValueError('You must specify a folder') elif not utils.is_list_like(folder): folder = [folder] * len(entities) elif len(entities) != len(folder): raise ValueError('Number of folders does not match number of entities') return await self(functions.folders.EditPeerFoldersRequest([ types.InputFolderPeer(x, folder_id=y) for x, y in zip(entities, folder) ])) async def delete_dialog( self: 'TelegramClient', entity: 'hints.EntityLike', *, revoke: bool = False ): """ Deletes a dialog (leaves a chat or channel). This method can be used as a user and as a bot. However, bots will only be able to use it to leave groups and channels (trying to delete a private conversation will do nothing). See also `Dialog.delete() `. Arguments entity (entities): The entity of the dialog to delete. If it's a chat or channel, you will leave it. Note that the chat itself is not deleted, only the dialog, because you left it. revoke (`bool`, optional): On private chats, you may revoke the messages from the other peer too. By default, it's `False`. Set it to `True` to delete the history for both. This makes no difference for bot accounts, who can only leave groups and channels. Returns The :tl:`Updates` object that the request produces, or nothing for private conversations. Example .. code-block:: python # Deleting the first dialog dialogs = await client.get_dialogs(5) await client.delete_dialog(dialogs[0]) # Leaving a channel by username await client.delete_dialog('username') """ # If we have enough information (`Dialog.delete` gives it to us), # then we know we don't have to kick ourselves in deactivated chats. if isinstance(entity, types.Chat): deactivated = entity.deactivated else: deactivated = False entity = await self.get_input_entity(entity) ty = helpers._entity_type(entity) if ty == helpers._EntityType.CHANNEL: return await self(functions.channels.LeaveChannelRequest(entity)) if ty == helpers._EntityType.CHAT and not deactivated: try: result = await self(functions.messages.DeleteChatUserRequest( entity.chat_id, types.InputUserSelf(), revoke_history=revoke )) except errors.PeerIdInvalidError: # Happens if we didn't have the deactivated information result = None else: result = None if not await self.is_bot(): await self(functions.messages.DeleteHistoryRequest(entity, 0, revoke=revoke)) return result def conversation( self: 'TelegramClient', entity: 'hints.EntityLike', *, timeout: float = 60, total_timeout: float = None, max_messages: int = 100, exclusive: bool = True, replies_are_responses: bool = True) -> custom.Conversation: """ Creates a `Conversation ` with the given entity. .. note:: This Conversation API has certain shortcomings, such as lacking persistence, poor interaction with other event handlers, and overcomplicated usage for anything beyond the simplest case. If you plan to interact with a bot without handlers, this works fine, but when running a bot yourself, you may instead prefer to follow the advice from https://stackoverflow.com/a/62246569/. This is not the same as just sending a message to create a "dialog" with them, but rather a way to easily send messages and await for responses or other reactions. Refer to its documentation for more. Arguments entity (`entity`): The entity with which a new conversation should be opened. timeout (`int` | `float`, optional): The default timeout (in seconds) *per action* to be used. You may also override this timeout on a per-method basis. By default each action can take up to 60 seconds (the value of this timeout). total_timeout (`int` | `float`, optional): The total timeout (in seconds) to use for the whole conversation. This takes priority over per-action timeouts. After these many seconds pass, subsequent actions will result in ``asyncio.TimeoutError``. max_messages (`int`, optional): The maximum amount of messages this conversation will remember. After these many messages arrive in the specified chat, subsequent actions will result in ``ValueError``. exclusive (`bool`, optional): By default, conversations are exclusive within a single chat. That means that while a conversation is open in a chat, you can't open another one in the same chat, unless you disable this flag. If you try opening an exclusive conversation for a chat where it's already open, it will raise ``AlreadyInConversationError``. replies_are_responses (`bool`, optional): Whether replies should be treated as responses or not. If the setting is enabled, calls to `conv.get_response ` and a subsequent call to `conv.get_reply ` will return different messages, otherwise they may return the same message. Consider the following scenario with one outgoing message, 1, and two incoming messages, the second one replying:: Hello! <1 2> (reply to 1) Hi! 3> (reply to 1) How are you? And the following code: .. code-block:: python async with client.conversation(chat) as conv: msg1 = await conv.send_message('Hello!') msg2 = await conv.get_response() msg3 = await conv.get_reply() With the setting enabled, ``msg2`` will be ``'Hi!'`` and ``msg3`` be ``'How are you?'`` since replies are also responses, and a response was already returned. With the setting disabled, both ``msg2`` and ``msg3`` will be ``'Hi!'`` since one is a response and also a reply. Returns A `Conversation `. Example .. code-block:: python # denotes outgoing messages you sent # denotes incoming response messages with bot.conversation(chat) as conv: # Hi! conv.send_message('Hi!') # Hello! hello = conv.get_response() # Please tell me your name conv.send_message('Please tell me your name') # ? name = conv.get_response().raw_text while not any(x.isalpha() for x in name): # Your name didn't have any letters! Try again conv.send_message("Your name didn't have any letters! Try again") # Human name = conv.get_response().raw_text # Thanks Human! conv.send_message('Thanks {}!'.format(name)) """ return custom.Conversation( self, entity, timeout=timeout, total_timeout=total_timeout, max_messages=max_messages, exclusive=exclusive, replies_are_responses=replies_are_responses ) # endregion Telethon-1.39.0/telethon/client/downloads.py000066400000000000000000001213331475566265000210550ustar00rootroot00000000000000import datetime import io import os import pathlib import typing import inspect import asyncio from ..crypto import AES from .. import utils, helpers, errors, hints from ..requestiter import RequestIter from ..tl import TLObject, types, functions try: import aiohttp except ImportError: aiohttp = None if typing.TYPE_CHECKING: from .telegramclient import TelegramClient # Chunk sizes for upload.getFile must be multiples of the smallest size MIN_CHUNK_SIZE = 4096 MAX_CHUNK_SIZE = 512 * 1024 # 2021-01-15, users reported that `errors.TimeoutError` can occur while downloading files. TIMED_OUT_SLEEP = 1 class _CdnRedirect(Exception): def __init__(self, cdn_redirect=None): self.cdn_redirect = cdn_redirect class _DirectDownloadIter(RequestIter): async def _init( self, file, dc_id, offset, stride, chunk_size, request_size, file_size, msg_data, cdn_redirect=None): self.request = functions.upload.GetFileRequest( file, offset=offset, limit=request_size) self._client = self.client self._cdn_redirect = cdn_redirect if cdn_redirect is not None: self.request = functions.upload.GetCdnFileRequest(cdn_redirect.file_token, offset=offset, limit=request_size) self._client = await self.client._get_cdn_client(cdn_redirect) self.total = file_size self._stride = stride self._chunk_size = chunk_size self._last_part = None self._msg_data = msg_data self._timed_out = False self._exported = dc_id and self._client.session.dc_id != dc_id if not self._exported: # The used sender will also change if ``FileMigrateError`` occurs self._sender = self.client._sender else: try: self._sender = await self.client._borrow_exported_sender(dc_id) except errors.DcIdInvalidError: # Can't export a sender for the ID we are currently in config = await self.client(functions.help.GetConfigRequest()) for option in config.dc_options: if option.ip_address == self.client.session.server_address: self.client.session.set_dc( option.id, option.ip_address, option.port) self.client.session.save() break # TODO Figure out why the session may have the wrong DC ID self._sender = self.client._sender self._exported = False async def _load_next_chunk(self): cur = await self._request() self.buffer.append(cur) if len(cur) < self.request.limit: self.left = len(self.buffer) await self.close() else: self.request.offset += self._stride async def _request(self): try: result = await self._client._call(self._sender, self.request) self._timed_out = False if isinstance(result, types.upload.FileCdnRedirect): if self.client._mb_entity_cache.self_bot: raise ValueError('FileCdnRedirect but the GetCdnFileRequest API access for bot users is restricted. Try to change api_id to avoid FileCdnRedirect') raise _CdnRedirect(result) if isinstance(result, types.upload.CdnFileReuploadNeeded): await self.client._call(self.client._sender, functions.upload.ReuploadCdnFileRequest(file_token=self._cdn_redirect.file_token, request_token=result.request_token)) result = await self._client._call(self._sender, self.request) return result.bytes else: return result.bytes except errors.TimedOutError as e: if self._timed_out: self.client._log[__name__].warning('Got two timeouts in a row while downloading file') raise self._timed_out = True self.client._log[__name__].info('Got timeout while downloading file, retrying once') await asyncio.sleep(TIMED_OUT_SLEEP) return await self._request() except errors.FileMigrateError as e: self.client._log[__name__].info('File lives in another DC') self._sender = await self.client._borrow_exported_sender(e.new_dc) self._exported = True return await self._request() except (errors.FilerefUpgradeNeededError, errors.FileReferenceExpiredError) as e: # Only implemented for documents which are the ones that may take that long to download if not self._msg_data \ or not isinstance(self.request.location, types.InputDocumentFileLocation) \ or self.request.location.thumb_size != '': raise self.client._log[__name__].info('File ref expired during download; refetching message') chat, msg_id = self._msg_data msg = await self.client.get_messages(chat, ids=msg_id) if not isinstance(msg.media, types.MessageMediaDocument): raise document = msg.media.document # Message media may have been edited for something else if document.id != self.request.location.id: raise self.request.location.file_reference = document.file_reference return await self._request() async def close(self): if not self._sender: return try: if self._exported: await self.client._return_exported_sender(self._sender) elif self._sender != self.client._sender: await self._sender.disconnect() finally: self._sender = None async def __aenter__(self): return self async def __aexit__(self, *args): await self.close() __enter__ = helpers._sync_enter __exit__ = helpers._sync_exit class _GenericDownloadIter(_DirectDownloadIter): async def _load_next_chunk(self): # 1. Fetch enough for one chunk data = b'' # 1.1. ``bad`` is how much into the data we have we need to offset bad = self.request.offset % self.request.limit before = self.request.offset # 1.2. We have to fetch from a valid offset, so remove that bad part self.request.offset -= bad done = False while not done and len(data) - bad < self._chunk_size: cur = await self._request() self.request.offset += self.request.limit data += cur done = len(cur) < self.request.limit # 1.3 Restore our last desired offset self.request.offset = before # 2. Fill the buffer with the data we have # 2.1. Slicing `bytes` is expensive, yield `memoryview` instead mem = memoryview(data) # 2.2. The current chunk starts at ``bad`` offset into the data, # and each new chunk is ``stride`` bytes apart of the other for i in range(bad, len(data), self._stride): self.buffer.append(mem[i:i + self._chunk_size]) # 2.3. We will yield this offset, so move to the next one self.request.offset += self._stride # 2.4. If we are in the last chunk, we will return the last partial data if done: self.left = len(self.buffer) await self.close() return # 2.5. If we are not done, we can't return incomplete chunks. if len(self.buffer[-1]) != self._chunk_size: self._last_part = self.buffer.pop().tobytes() # 3. Be careful with the offsets. Re-fetching a bit of data # is fine, since it greatly simplifies things. # TODO Try to not re-fetch data self.request.offset -= self._stride class DownloadMethods: # region Public methods async def download_profile_photo( self: 'TelegramClient', entity: 'hints.EntityLike', file: 'hints.FileLike' = None, *, download_big: bool = True) -> typing.Optional[str]: """ Downloads the profile photo from the given user, chat or channel. Arguments entity (`entity`): From who the photo will be downloaded. .. note:: This method expects the full entity (which has the data to download the photo), not an input variant. It's possible that sometimes you can't fetch the entity from its input (since you can get errors like ``ChannelPrivateError``) but you already have it through another call, like getting a forwarded message from it. file (`str` | `file`, optional): The output file path, directory, or stream-like object. If the path exists and is a file, it will be overwritten. If file is the type `bytes`, it will be downloaded in-memory and returned as a bytestring (i.e. ``file=bytes``, without parentheses or quotes). download_big (`bool`, optional): Whether to use the big version of the available photos. Returns `None` if no photo was provided, or if it was Empty. On success the file path is returned since it may differ from the one given. Example .. code-block:: python # Download your own profile photo path = await client.download_profile_photo('me') print(path) """ # hex(crc32(x.encode('ascii'))) for x in # ('User', 'Chat', 'UserFull', 'ChatFull') ENTITIES = (0x2da17977, 0xc5af5d94, 0x1f4661b9, 0xd49a2697) # ('InputPeer', 'InputUser', 'InputChannel') INPUTS = (0xc91c90b6, 0xe669bf46, 0x40f202fd) if not isinstance(entity, TLObject) or entity.SUBCLASS_OF_ID in INPUTS: entity = await self.get_entity(entity) thumb = -1 if download_big else 0 possible_names = [] if entity.SUBCLASS_OF_ID not in ENTITIES: photo = entity else: if not hasattr(entity, 'photo'): # Special case: may be a ChatFull with photo:Photo # This is different from a normal UserProfilePhoto and Chat if not hasattr(entity, 'chat_photo'): return None return await self._download_photo( entity.chat_photo, file, date=None, thumb=thumb, progress_callback=None ) for attr in ('username', 'first_name', 'title'): possible_names.append(getattr(entity, attr, None)) photo = entity.photo if isinstance(photo, (types.UserProfilePhoto, types.ChatPhoto)): dc_id = photo.dc_id loc = types.InputPeerPhotoFileLocation( # min users can be used to download profile photos # self.get_input_entity would otherwise not accept those peer=utils.get_input_peer(entity, check_hash=False), photo_id=photo.photo_id, big=download_big ) else: # It doesn't make any sense to check if `photo` can be used # as input location, because then this method would be able # to "download the profile photo of a message", i.e. its # media which should be done with `download_media` instead. return None file = self._get_proper_filename( file, 'profile_photo', '.jpg', possible_names=possible_names ) try: result = await self.download_file(loc, file, dc_id=dc_id) return result if file is bytes else file except errors.LocationInvalidError: # See issue #500, Android app fails as of v4.6.0 (1155). # The fix seems to be using the full channel chat photo. ie = await self.get_input_entity(entity) ty = helpers._entity_type(ie) if ty == helpers._EntityType.CHANNEL: full = await self(functions.channels.GetFullChannelRequest(ie)) return await self._download_photo( full.full_chat.chat_photo, file, date=None, progress_callback=None, thumb=thumb ) else: # Until there's a report for chats, no need to. return None async def download_media( self: 'TelegramClient', message: 'hints.MessageLike', file: 'hints.FileLike' = None, *, thumb: 'typing.Union[int, types.TypePhotoSize]' = None, progress_callback: 'hints.ProgressCallback' = None) -> typing.Optional[typing.Union[str, bytes]]: """ Downloads the given media from a message object. Note that if the download is too slow, you should consider installing ``cryptg`` (through ``pip install cryptg``) so that decrypting the received data is done in C instead of Python (much faster). See also `Message.download_media() `. Arguments message (`Message ` | :tl:`Media`): The media or message containing the media that will be downloaded. file (`str` | `file`, optional): The output file path, directory, or stream-like object. If the path exists and is a file, it will be overwritten. If file is the type `bytes`, it will be downloaded in-memory and returned as a bytestring (i.e. ``file=bytes``, without parentheses or quotes). progress_callback (`callable`, optional): A callback function accepting two parameters: ``(received bytes, total)``. thumb (`int` | :tl:`PhotoSize`, optional): Which thumbnail size from the document or photo to download, instead of downloading the document or photo itself. If it's specified but the file does not have a thumbnail, this method will return `None`. The parameter should be an integer index between ``0`` and ``len(sizes)``. ``0`` will download the smallest thumbnail, and ``len(sizes) - 1`` will download the largest thumbnail. You can also use negative indices, which work the same as they do in Python's `list`. You can also pass the :tl:`PhotoSize` instance to use. Alternatively, the thumb size type `str` may be used. In short, use ``thumb=0`` if you want the smallest thumbnail and ``thumb=-1`` if you want the largest thumbnail. .. note:: The largest thumbnail may be a video instead of a photo, as they are available since layer 116 and are bigger than any of the photos. Returns `None` if no media was provided, or if it was Empty. On success the file path is returned since it may differ from the one given. Example .. code-block:: python path = await client.download_media(message) await client.download_media(message, filename) # or path = await message.download_media() await message.download_media(filename) # Downloading to memory blob = await client.download_media(message, bytes) # Printing download progress def callback(current, total): print('Downloaded', current, 'out of', total, 'bytes: {:.2%}'.format(current / total)) await client.download_media(message, progress_callback=callback) """ # Downloading large documents may be slow enough to require a new file reference # to be obtained mid-download. Store (input chat, message id) so that the message # can be re-fetched. msg_data = None # TODO This won't work for messageService if isinstance(message, types.Message): date = message.date media = message.media msg_data = (message.input_chat, message.id) if message.input_chat else None else: date = datetime.datetime.now() media = message if isinstance(media, str): media = utils.resolve_bot_file_id(media) if isinstance(media, types.MessageService): if isinstance(message.action, types.MessageActionChatEditPhoto): media = media.photo if isinstance(media, types.MessageMediaWebPage): if isinstance(media.webpage, types.WebPage): media = media.webpage.document or media.webpage.photo if isinstance(media, (types.MessageMediaPhoto, types.Photo)): return await self._download_photo( media, file, date, thumb, progress_callback ) elif isinstance(media, (types.MessageMediaDocument, types.Document)): return await self._download_document( media, file, date, thumb, progress_callback, msg_data ) elif isinstance(media, types.MessageMediaContact) and thumb is None: return self._download_contact( media, file ) elif isinstance(media, (types.WebDocument, types.WebDocumentNoProxy)) and thumb is None: return await self._download_web_document( media, file, progress_callback ) async def download_file( self: 'TelegramClient', input_location: 'hints.FileLike', file: 'hints.OutFileLike' = None, *, part_size_kb: float = None, file_size: int = None, progress_callback: 'hints.ProgressCallback' = None, dc_id: int = None, key: bytes = None, iv: bytes = None) -> typing.Optional[bytes]: """ Low-level method to download files from their input location. .. note:: Generally, you should instead use `download_media`. This method is intended to be a bit more low-level. Arguments input_location (:tl:`InputFileLocation`): The file location from which the file will be downloaded. See `telethon.utils.get_input_location` source for a complete list of supported types. file (`str` | `file`, optional): The output file path, directory, or stream-like object. If the path exists and is a file, it will be overwritten. If the file path is `None` or `bytes`, then the result will be saved in memory and returned as `bytes`. part_size_kb (`int`, optional): Chunk size when downloading files. The larger, the less requests will be made (up to 512KB maximum). file_size (`int`, optional): The file size that is about to be downloaded, if known. Only used if ``progress_callback`` is specified. progress_callback (`callable`, optional): A callback function accepting two parameters: ``(downloaded bytes, total)``. Note that the ``total`` is the provided ``file_size``. dc_id (`int`, optional): The data center the library should connect to in order to download the file. You shouldn't worry about this. key ('bytes', optional): In case of an encrypted upload (secret chats) a key is supplied iv ('bytes', optional): In case of an encrypted upload (secret chats) an iv is supplied Example .. code-block:: python # Download a file and print its header data = await client.download_file(input_file, bytes) print(data[:16]) """ return await self._download_file( input_location, file, part_size_kb=part_size_kb, file_size=file_size, progress_callback=progress_callback, dc_id=dc_id, key=key, iv=iv, ) async def _download_file( self: 'TelegramClient', input_location: 'hints.FileLike', file: 'hints.OutFileLike' = None, *, part_size_kb: float = None, file_size: int = None, progress_callback: 'hints.ProgressCallback' = None, dc_id: int = None, key: bytes = None, iv: bytes = None, msg_data: tuple = None, cdn_redirect: types.upload.FileCdnRedirect = None ) -> typing.Optional[bytes]: if not part_size_kb: if not file_size: part_size_kb = 64 # Reasonable default else: part_size_kb = utils.get_appropriated_part_size(file_size) part_size = int(part_size_kb * 1024) if part_size % MIN_CHUNK_SIZE != 0: raise ValueError( 'The part size must be evenly divisible by 4096.') if isinstance(file, pathlib.Path): file = str(file.absolute()) in_memory = file is None or file is bytes if in_memory: f = io.BytesIO() elif isinstance(file, str): # Ensure that we'll be able to download the media helpers.ensure_parent_dir_exists(file) f = open(file, 'wb') else: f = file try: async for chunk in self._iter_download( input_location, request_size=part_size, dc_id=dc_id, msg_data=msg_data, cdn_redirect=cdn_redirect): if iv and key: chunk = AES.decrypt_ige(chunk, key, iv) r = f.write(chunk) if inspect.isawaitable(r): await r if progress_callback: r = progress_callback(f.tell(), file_size) if inspect.isawaitable(r): await r # Not all IO objects have flush (see #1227) if callable(getattr(f, 'flush', None)): f.flush() if in_memory: return f.getvalue() except _CdnRedirect as e: self._log[__name__].info('FileCdnRedirect to CDN data center %s', e.cdn_redirect.dc_id) return await self._download_file( input_location=input_location, file=file, part_size_kb=part_size_kb, file_size=file_size, progress_callback=progress_callback, dc_id=e.cdn_redirect.dc_id, key=e.cdn_redirect.encryption_key, iv=e.cdn_redirect.encryption_iv, msg_data=msg_data, cdn_redirect=e.cdn_redirect ) finally: if isinstance(file, str) or in_memory: f.close() def iter_download( self: 'TelegramClient', file: 'hints.FileLike', *, offset: int = 0, stride: int = None, limit: int = None, chunk_size: int = None, request_size: int = MAX_CHUNK_SIZE, file_size: int = None, dc_id: int = None ): """ Iterates over a file download, yielding chunks of the file. This method can be used to stream files in a more convenient way, since it offers more control (pausing, resuming, etc.) .. note:: Using a value for `offset` or `stride` which is not a multiple of the minimum allowed `request_size`, or if `chunk_size` is different from `request_size`, the library will need to do a bit more work to fetch the data in the way you intend it to. You normally shouldn't worry about this. Arguments file (`hints.FileLike`): The file of which contents you want to iterate over. offset (`int`, optional): The offset in bytes into the file from where the download should start. For example, if a file is 1024KB long and you just want the last 512KB, you would use ``offset=512 * 1024``. stride (`int`, optional): The stride of each chunk (how much the offset should advance between reading each chunk). This parameter should only be used for more advanced use cases. It must be bigger than or equal to the `chunk_size`. limit (`int`, optional): The limit for how many *chunks* will be yielded at most. chunk_size (`int`, optional): The maximum size of the chunks that will be yielded. Note that the last chunk may be less than this value. By default, it equals to `request_size`. request_size (`int`, optional): How many bytes will be requested to Telegram when more data is required. By default, as many bytes as possible are requested. If you would like to request data in smaller sizes, adjust this parameter. Note that values outside the valid range will be clamped, and the final value will also be a multiple of the minimum allowed size. file_size (`int`, optional): If the file size is known beforehand, you should set this parameter to said value. Depending on the type of the input file passed, this may be set automatically. dc_id (`int`, optional): The data center the library should connect to in order to download the file. You shouldn't worry about this. Yields `bytes` objects representing the chunks of the file if the right conditions are met, or `memoryview` objects instead. Example .. code-block:: python # Streaming `media` to an output file # After the iteration ends, the sender is cleaned up with open('photo.jpg', 'wb') as fd: async for chunk in client.iter_download(media): fd.write(chunk) # Fetching only the header of a file (32 bytes) # You should manually close the iterator in this case. # # "stream" is a common name for asynchronous generators, # and iter_download will yield `bytes` (chunks of the file). stream = client.iter_download(media, request_size=32) header = await stream.__anext__() # "manual" version of `async for` await stream.close() assert len(header) == 32 """ return self._iter_download( file, offset=offset, stride=stride, limit=limit, chunk_size=chunk_size, request_size=request_size, file_size=file_size, dc_id=dc_id, ) def _iter_download( self: 'TelegramClient', file: 'hints.FileLike', *, offset: int = 0, stride: int = None, limit: int = None, chunk_size: int = None, request_size: int = MAX_CHUNK_SIZE, file_size: int = None, dc_id: int = None, msg_data: tuple = None, cdn_redirect: types.upload.FileCdnRedirect = None ): info = utils._get_file_info(file) if info.dc_id is not None: dc_id = info.dc_id if file_size is None: file_size = info.size file = info.location if chunk_size is None: chunk_size = request_size if limit is None and file_size is not None: limit = (file_size + chunk_size - 1) // chunk_size if stride is None: stride = chunk_size elif stride < chunk_size: raise ValueError('stride must be >= chunk_size') request_size -= request_size % MIN_CHUNK_SIZE if request_size < MIN_CHUNK_SIZE: request_size = MIN_CHUNK_SIZE elif request_size > MAX_CHUNK_SIZE: request_size = MAX_CHUNK_SIZE if chunk_size == request_size \ and offset % MIN_CHUNK_SIZE == 0 \ and stride % MIN_CHUNK_SIZE == 0 \ and (limit is None or offset % limit == 0): cls = _DirectDownloadIter self._log[__name__].info('Starting direct file download in chunks of ' '%d at %d, stride %d', request_size, offset, stride) else: cls = _GenericDownloadIter self._log[__name__].info('Starting indirect file download in chunks of ' '%d at %d, stride %d', request_size, offset, stride) return cls( self, limit, file=file, dc_id=dc_id, offset=offset, stride=stride, chunk_size=chunk_size, request_size=request_size, file_size=file_size, msg_data=msg_data, cdn_redirect=cdn_redirect ) # endregion # region Private methods @staticmethod def _get_thumb(thumbs, thumb): if not thumbs: return None # Seems Telegram has changed the order and put `PhotoStrippedSize` # last while this is the smallest (layer 116). Ensure we have the # sizes sorted correctly with a custom function. def sort_thumbs(thumb): if isinstance(thumb, types.PhotoStrippedSize): return 1, len(thumb.bytes) if isinstance(thumb, types.PhotoCachedSize): return 1, len(thumb.bytes) if isinstance(thumb, types.PhotoSize): return 1, thumb.size if isinstance(thumb, types.PhotoSizeProgressive): return 1, max(thumb.sizes) if isinstance(thumb, types.VideoSize): return 2, thumb.size # Empty size or invalid should go last return 0, 0 thumbs = list(sorted(thumbs, key=sort_thumbs)) for i in reversed(range(len(thumbs))): # :tl:`PhotoPathSize` is used for animated stickers preview, and the thumb is actually # a SVG path of the outline. Users expect thumbnails to be JPEG files, so pretend this # thumb size doesn't actually exist (#1655). if isinstance(thumbs[i], types.PhotoPathSize): thumbs.pop(i) if thumb is None: return thumbs[-1] elif isinstance(thumb, int): return thumbs[thumb] elif isinstance(thumb, str): return next((t for t in thumbs if t.type == thumb), None) elif isinstance(thumb, (types.PhotoSize, types.PhotoCachedSize, types.PhotoStrippedSize, types.VideoSize)): return thumb else: return None def _download_cached_photo_size(self: 'TelegramClient', size, file): # No need to download anything, simply write the bytes if isinstance(size, types.PhotoStrippedSize): data = utils.stripped_photo_to_jpg(size.bytes) else: data = size.bytes if file is bytes: return data elif isinstance(file, str): helpers.ensure_parent_dir_exists(file) f = open(file, 'wb') else: f = file try: f.write(data) finally: if isinstance(file, str): f.close() return file async def _download_photo(self: 'TelegramClient', photo, file, date, thumb, progress_callback): """Specialized version of .download_media() for photos""" # Determine the photo and its largest size if isinstance(photo, types.MessageMediaPhoto): photo = photo.photo if not isinstance(photo, types.Photo): return # Include video sizes here (but they may be None so provide an empty list) size = self._get_thumb(photo.sizes + (photo.video_sizes or []), thumb) if not size or isinstance(size, types.PhotoSizeEmpty): return if isinstance(size, types.VideoSize): file = self._get_proper_filename(file, 'video', '.mp4', date=date) else: file = self._get_proper_filename(file, 'photo', '.jpg', date=date) if isinstance(size, (types.PhotoCachedSize, types.PhotoStrippedSize)): return self._download_cached_photo_size(size, file) if isinstance(size, types.PhotoSizeProgressive): file_size = max(size.sizes) else: file_size = size.size result = await self.download_file( types.InputPhotoFileLocation( id=photo.id, access_hash=photo.access_hash, file_reference=photo.file_reference, thumb_size=size.type ), file, file_size=file_size, progress_callback=progress_callback ) return result if file is bytes else file @staticmethod def _get_kind_and_names(attributes): """Gets kind and possible names for :tl:`DocumentAttribute`.""" kind = 'document' possible_names = [] for attr in attributes: if isinstance(attr, types.DocumentAttributeFilename): possible_names.insert(0, attr.file_name) elif isinstance(attr, types.DocumentAttributeAudio): kind = 'audio' if attr.performer and attr.title: possible_names.append('{} - {}'.format( attr.performer, attr.title )) elif attr.performer: possible_names.append(attr.performer) elif attr.title: possible_names.append(attr.title) elif attr.voice: kind = 'voice' return kind, possible_names async def _download_document( self, document, file, date, thumb, progress_callback, msg_data): """Specialized version of .download_media() for documents.""" if isinstance(document, types.MessageMediaDocument): document = document.document if not isinstance(document, types.Document): return if thumb is None: kind, possible_names = self._get_kind_and_names(document.attributes) file = self._get_proper_filename( file, kind, utils.get_extension(document), date=date, possible_names=possible_names ) size = None else: file = self._get_proper_filename(file, 'photo', '.jpg', date=date) size = self._get_thumb(document.thumbs, thumb) if not size or isinstance(size, types.PhotoSizeEmpty): return if isinstance(size, (types.PhotoCachedSize, types.PhotoStrippedSize)): return self._download_cached_photo_size(size, file) result = await self._download_file( types.InputDocumentFileLocation( id=document.id, access_hash=document.access_hash, file_reference=document.file_reference, thumb_size=size.type if size else '' ), file, file_size=size.size if size else document.size, progress_callback=progress_callback, msg_data=msg_data, ) return result if file is bytes else file @classmethod def _download_contact(cls, mm_contact, file): """ Specialized version of .download_media() for contacts. Will make use of the vCard 4.0 format. """ first_name = mm_contact.first_name last_name = mm_contact.last_name phone_number = mm_contact.phone_number # Remove these pesky characters first_name = first_name.replace(';', '') last_name = (last_name or '').replace(';', '') result = ( 'BEGIN:VCARD\n' 'VERSION:4.0\n' 'N:{f};{l};;;\n' 'FN:{f} {l}\n' 'TEL;TYPE=cell;VALUE=uri:tel:+{p}\n' 'END:VCARD\n' ).format(f=first_name, l=last_name, p=phone_number).encode('utf-8') file = cls._get_proper_filename( file, 'contact', '.vcard', possible_names=[first_name, phone_number, last_name] ) if file is bytes: return result f = file if hasattr(file, 'write') else open(file, 'wb') try: f.write(result) finally: # Only close the stream if we opened it if f != file: f.close() return file @classmethod async def _download_web_document(cls, web, file, progress_callback): """ Specialized version of .download_media() for web documents. """ if not aiohttp: raise ValueError( 'Cannot download web documents without the aiohttp ' 'dependency install it (pip install aiohttp)' ) # TODO Better way to get opened handles of files and auto-close kind, possible_names = self._get_kind_and_names(web.attributes) file = self._get_proper_filename( file, kind, utils.get_extension(web), possible_names=possible_names ) if file is bytes: f = io.BytesIO() elif hasattr(file, 'write'): f = file else: f = open(file, 'wb') try: async with aiohttp.ClientSession() as session: # TODO Use progress_callback; get content length from response # https://github.com/telegramdesktop/tdesktop/blob/c7e773dd9aeba94e2be48c032edc9a78bb50234e/Telegram/SourceFiles/ui/images.cpp#L1318-L1319 async with session.get(web.url) as response: while True: chunk = await response.content.read(128 * 1024) if not chunk: break f.write(chunk) finally: if f != file: f.close() return f.getvalue() if file is bytes else file @staticmethod def _get_proper_filename(file, kind, extension, date=None, possible_names=None): """Gets a proper filename for 'file', if this is a path. 'kind' should be the kind of the output file (photo, document...) 'extension' should be the extension to be added to the file if the filename doesn't have any yet 'date' should be when this file was originally sent, if known 'possible_names' should be an ordered list of possible names If no modification is made to the path, any existing file will be overwritten. If any modification is made to the path, this method will ensure that no existing file will be overwritten. """ if isinstance(file, pathlib.Path): file = str(file.absolute()) if file is not None and not isinstance(file, str): # Probably a stream-like object, we cannot set a filename here return file if file is None: file = '' elif os.path.isfile(file): # Make no modifications to valid existing paths return file if os.path.isdir(file) or not file: try: name = None if possible_names is None else next( x for x in possible_names if x ) except StopIteration: name = None if not name: if not date: date = datetime.datetime.now() name = '{}_{}-{:02}-{:02}_{:02}-{:02}-{:02}'.format( kind, date.year, date.month, date.day, date.hour, date.minute, date.second, ) file = os.path.join(file, name) directory, name = os.path.split(file) name, ext = os.path.splitext(name) if not ext: ext = extension result = os.path.join(directory, name + ext) if not os.path.isfile(result): return result i = 1 while True: result = os.path.join(directory, '{} ({}){}'.format(name, i, ext)) if not os.path.isfile(result): return result i += 1 # endregion Telethon-1.39.0/telethon/client/messageparse.py000066400000000000000000000222531475566265000215430ustar00rootroot00000000000000import itertools import re import typing from .. import helpers, utils from ..tl import types if typing.TYPE_CHECKING: from .telegramclient import TelegramClient class MessageParseMethods: # region Public properties @property def parse_mode(self: 'TelegramClient'): """ This property is the default parse mode used when sending messages. Defaults to `telethon.extensions.markdown`. It will always be either `None` or an object with ``parse`` and ``unparse`` methods. When setting a different value it should be one of: * Object with ``parse`` and ``unparse`` methods. * A ``callable`` to act as the parse method. * A `str` indicating the ``parse_mode``. For Markdown ``'md'`` or ``'markdown'`` may be used. For HTML, ``'htm'`` or ``'html'`` may be used. The ``parse`` method should be a function accepting a single parameter, the text to parse, and returning a tuple consisting of ``(parsed message str, [MessageEntity instances])``. The ``unparse`` method should be the inverse of ``parse`` such that ``assert text == unparse(*parse(text))``. See :tl:`MessageEntity` for allowed message entities. Example .. code-block:: python # Disabling default formatting client.parse_mode = None # Enabling HTML as the default format client.parse_mode = 'html' """ return self._parse_mode @parse_mode.setter def parse_mode(self: 'TelegramClient', mode: str): self._parse_mode = utils.sanitize_parse_mode(mode) # endregion # region Private methods async def _replace_with_mention(self: 'TelegramClient', entities, i, user): """ Helper method to replace ``entities[i]`` to mention ``user``, or do nothing if it can't be found. """ try: entities[i] = types.InputMessageEntityMentionName( entities[i].offset, entities[i].length, await self.get_input_entity(user) ) return True except (ValueError, TypeError): return False async def _parse_message_text(self: 'TelegramClient', message, parse_mode): """ Returns a (parsed message, entities) tuple depending on ``parse_mode``. """ if parse_mode == (): parse_mode = self._parse_mode else: parse_mode = utils.sanitize_parse_mode(parse_mode) if not parse_mode: return message, [] original_message = message message, msg_entities = parse_mode.parse(message) if original_message and not message and not msg_entities: raise ValueError("Failed to parse message") for i in reversed(range(len(msg_entities))): e = msg_entities[i] if not e.length: # 0-length MessageEntity is no longer valid #3884. # Because the user can provide their own parser (with reasonable 0-length # entities), strip them here rather than fixing the built-in parsers. del msg_entities[i] elif isinstance(e, types.MessageEntityTextUrl): m = re.match(r'^@|\+|tg://user\?id=(\d+)', e.url) if m: user = int(m.group(1)) if m.group(1) else e.url is_mention = await self._replace_with_mention(msg_entities, i, user) if not is_mention: del msg_entities[i] elif isinstance(e, (types.MessageEntityMentionName, types.InputMessageEntityMentionName)): is_mention = await self._replace_with_mention(msg_entities, i, e.user_id) if not is_mention: del msg_entities[i] return message, msg_entities def _get_response_message(self: 'TelegramClient', request, result, input_chat): """ Extracts the response message known a request and Update result. The request may also be the ID of the message to match. If ``request is None`` this method returns ``{id: message}``. If ``request.random_id`` is a list, this method returns a list too. """ if isinstance(result, types.UpdateShort): updates = [result.update] entities = {} elif isinstance(result, (types.Updates, types.UpdatesCombined)): updates = result.updates entities = {utils.get_peer_id(x): x for x in itertools.chain(result.users, result.chats)} else: return None random_to_id = {} id_to_message = {} for update in updates: if isinstance(update, types.UpdateMessageID): random_to_id[update.random_id] = update.id elif isinstance(update, ( types.UpdateNewChannelMessage, types.UpdateNewMessage)): update.message._finish_init(self, entities, input_chat) # Pinning a message with `updatePinnedMessage` seems to # always produce a service message we can't map so return # it directly. The same happens for kicking users. # # It could also be a list (e.g. when sending albums). # # TODO this method is getting messier and messier as time goes on if hasattr(request, 'random_id') or utils.is_list_like(request): id_to_message[update.message.id] = update.message else: return update.message elif (isinstance(update, types.UpdateEditMessage) and helpers._entity_type(request.peer) != helpers._EntityType.CHANNEL): update.message._finish_init(self, entities, input_chat) # Live locations use `sendMedia` but Telegram responds with # `updateEditMessage`, which means we won't have `id` field. if hasattr(request, 'random_id'): id_to_message[update.message.id] = update.message elif request.id == update.message.id: return update.message elif (isinstance(update, types.UpdateEditChannelMessage) and utils.get_peer_id(request.peer) == utils.get_peer_id(update.message.peer_id)): if request.id == update.message.id: update.message._finish_init(self, entities, input_chat) return update.message elif isinstance(update, types.UpdateNewScheduledMessage): update.message._finish_init(self, entities, input_chat) # Scheduled IDs may collide with normal IDs. However, for a # single request there *shouldn't* be a mix between "some # scheduled and some not". id_to_message[update.message.id] = update.message elif isinstance(update, types.UpdateMessagePoll): if request.media.poll.id == update.poll_id: m = types.Message( id=request.id, peer_id=utils.get_peer(request.peer), media=types.MessageMediaPoll( poll=update.poll, results=update.results ) ) m._finish_init(self, entities, input_chat) return m if request is None: return id_to_message random_id = request if isinstance(request, (int, list)) else getattr(request, 'random_id', None) if random_id is None: # Can happen when pinning a message does not actually produce a service message. self._log[__name__].warning( 'No random_id in %s to map to, returning None message for %s', request, result) return None if not utils.is_list_like(random_id): msg = id_to_message.get(random_to_id.get(random_id)) if not msg: self._log[__name__].warning( 'Request %s had missing message mapping %s', request, result) return msg try: return [id_to_message[random_to_id[rnd]] for rnd in random_id] except KeyError: # Sometimes forwards fail (`MESSAGE_ID_INVALID` if a message gets # deleted or `WORKER_BUSY_TOO_LONG_RETRY` if there are issues at # Telegram), in which case we get some "missing" message mappings. # Log them with the hope that we can better work around them. # # This also happens when trying to forward messages that can't # be forwarded because they don't exist (0, service, deleted) # among others which could be (like deleted or existing). self._log[__name__].warning( 'Request %s had missing message mappings %s', request, result) return [ id_to_message.get(random_to_id[rnd]) if rnd in random_to_id else None for rnd in random_id ] # endregion Telethon-1.39.0/telethon/client/messages.py000066400000000000000000001740721475566265000207020ustar00rootroot00000000000000import inspect import itertools import typing import warnings from .. import helpers, utils, errors, hints from ..requestiter import RequestIter from ..tl import types, functions _MAX_CHUNK_SIZE = 100 if typing.TYPE_CHECKING: from .telegramclient import TelegramClient class _MessagesIter(RequestIter): """ Common factor for all requests that need to iterate over messages. """ async def _init( self, entity, offset_id, min_id, max_id, from_user, offset_date, add_offset, filter, search, reply_to, scheduled ): # Note that entity being `None` will perform a global search. if entity: self.entity = await self.client.get_input_entity(entity) else: self.entity = None if self.reverse: raise ValueError('Cannot reverse global search') # Telegram doesn't like min_id/max_id. If these IDs are low enough # (starting from last_id - 100), the request will return nothing. # # We can emulate their behaviour locally by setting offset = max_id # and simply stopping once we hit a message with ID <= min_id. if self.reverse: offset_id = max(offset_id, min_id) if offset_id and max_id: if max_id - offset_id <= 1: raise StopAsyncIteration if not max_id: max_id = float('inf') else: offset_id = max(offset_id, max_id) if offset_id and min_id: if offset_id - min_id <= 1: raise StopAsyncIteration if self.reverse: if offset_id: offset_id += 1 elif not offset_date: # offset_id has priority over offset_date, so don't # set offset_id to 1 if we want to offset by date. offset_id = 1 if from_user: from_user = await self.client.get_input_entity(from_user) self.from_id = await self.client.get_peer_id(from_user) else: self.from_id = None # `messages.searchGlobal` only works with text `search` or `filter` queries. # If we want to perform global a search with `from_user` we have to perform # a normal `messages.search`, *but* we can make the entity be `inputPeerEmpty`. if not self.entity and from_user: self.entity = types.InputPeerEmpty() if filter is None: filter = types.InputMessagesFilterEmpty() else: filter = filter() if isinstance(filter, type) else filter if not self.entity: self.request = functions.messages.SearchGlobalRequest( q=search or '', filter=filter, min_date=None, max_date=offset_date, offset_rate=0, offset_peer=types.InputPeerEmpty(), offset_id=offset_id, limit=1 ) elif scheduled: self.request = functions.messages.GetScheduledHistoryRequest( peer=entity, hash=0 ) elif reply_to is not None: self.request = functions.messages.GetRepliesRequest( peer=self.entity, msg_id=reply_to, offset_id=offset_id, offset_date=offset_date, add_offset=add_offset, limit=1, max_id=0, min_id=0, hash=0 ) elif search is not None or not isinstance(filter, types.InputMessagesFilterEmpty) or from_user: # Telegram completely ignores `from_id` in private chats ty = helpers._entity_type(self.entity) if ty == helpers._EntityType.USER: # Don't bother sending `from_user` (it's ignored anyway), # but keep `from_id` defined above to check it locally. from_user = None else: # Do send `from_user` to do the filtering server-side, # and set `from_id` to None to avoid checking it locally. self.from_id = None self.request = functions.messages.SearchRequest( peer=self.entity, q=search or '', filter=filter, min_date=None, max_date=offset_date, offset_id=offset_id, add_offset=add_offset, limit=0, # Search actually returns 0 items if we ask it to max_id=0, min_id=0, hash=0, from_id=from_user ) # Workaround issue #1124 until a better solution is found. # Telegram seemingly ignores `max_date` if `filter` (and # nothing else) is specified, so we have to rely on doing # a first request to offset from the ID instead. # # Even better, using `filter` and `from_id` seems to always # trigger `RPC_CALL_FAIL` which is "internal issues"... if not isinstance(filter, types.InputMessagesFilterEmpty) \ and offset_date and not search and not offset_id: async for m in self.client.iter_messages( self.entity, 1, offset_date=offset_date): self.request.offset_id = m.id + 1 else: self.request = functions.messages.GetHistoryRequest( peer=self.entity, limit=1, offset_date=offset_date, offset_id=offset_id, min_id=0, max_id=0, add_offset=add_offset, hash=0 ) if self.limit <= 0: # No messages, but we still need to know the total message count result = await self.client(self.request) if isinstance(result, types.messages.MessagesNotModified): self.total = result.count else: self.total = getattr(result, 'count', len(result.messages)) raise StopAsyncIteration if self.wait_time is None: self.wait_time = 1 if self.limit > 3000 else 0 # When going in reverse we need an offset of `-limit`, but we # also want to respect what the user passed, so add them together. if self.reverse: self.request.add_offset -= _MAX_CHUNK_SIZE self.add_offset = add_offset self.max_id = max_id self.min_id = min_id self.last_id = 0 if self.reverse else float('inf') async def _load_next_chunk(self): self.request.limit = min(self.left, _MAX_CHUNK_SIZE) if self.reverse and self.request.limit != _MAX_CHUNK_SIZE: # Remember that we need -limit when going in reverse self.request.add_offset = self.add_offset - self.request.limit r = await self.client(self.request) self.total = getattr(r, 'count', len(r.messages)) entities = {utils.get_peer_id(x): x for x in itertools.chain(r.users, r.chats)} messages = reversed(r.messages) if self.reverse else r.messages for message in messages: if (isinstance(message, types.MessageEmpty) or self.from_id and message.sender_id != self.from_id): continue if not self._message_in_range(message): return True # There has been reports that on bad connections this method # was returning duplicated IDs sometimes. Using ``last_id`` # is an attempt to avoid these duplicates, since the message # IDs are returned in descending order (or asc if reverse). self.last_id = message.id message._finish_init(self.client, entities, self.entity) self.buffer.append(message) # Not a slice (using offset would return the same, with e.g. SearchGlobal). if isinstance(r, types.messages.Messages): return True # Some channels are "buggy" and may return less messages than # requested (apparently, the messages excluded are, for example, # "not displayable due to local laws"). # # This means it's not safe to rely on `len(r.messages) < req.limit` as # the stop condition. Unfortunately more requests must be made. # # However we can still check if the highest ID is equal to or lower # than the limit, in which case there won't be any more messages # because the lowest message ID is 1. # # We also assume the API will always return, at least, one message if # there is more to fetch. if not r.messages or (not self.reverse and r.messages[0].id <= self.request.limit): return True # Get the last message that's not empty (in some rare cases # it can happen that the last message is :tl:`MessageEmpty`) if self.buffer: self._update_offset(self.buffer[-1], r) else: # There are some cases where all the messages we get start # being empty. This can happen on migrated mega-groups if # the history was cleared, and we're using search. Telegram # acts incredibly weird sometimes. Messages are returned but # only "empty", not their contents. If this is the case we # should just give up since there won't be any new Message. return True def _message_in_range(self, message): """ Determine whether the given message is in the range or it should be ignored (and avoid loading more chunks). """ # No entity means message IDs between chats may vary if self.entity: if self.reverse: if message.id <= self.last_id or message.id >= self.max_id: return False else: if message.id >= self.last_id or message.id <= self.min_id: return False return True def _update_offset(self, last_message, response): """ After making the request, update its offset with the last message. """ self.request.offset_id = last_message.id if self.reverse: # We want to skip the one we already have self.request.offset_id += 1 if isinstance(self.request, functions.messages.SearchRequest): # Unlike getHistory and searchGlobal that use *offset* date, # this is *max* date. This means that doing a search in reverse # will break it. Since it's not really needed once we're going # (only for the first request), it's safe to just clear it off. self.request.max_date = None else: # getHistory, searchGlobal and getReplies call it offset_date self.request.offset_date = last_message.date if isinstance(self.request, functions.messages.SearchGlobalRequest): if last_message.input_chat: self.request.offset_peer = last_message.input_chat else: self.request.offset_peer = types.InputPeerEmpty() self.request.offset_rate = getattr(response, 'next_rate', 0) class _IDsIter(RequestIter): async def _init(self, entity, ids): self.total = len(ids) self._ids = list(reversed(ids)) if self.reverse else ids self._offset = 0 self._entity = (await self.client.get_input_entity(entity)) if entity else None self._ty = helpers._entity_type(self._entity) if self._entity else None # 30s flood wait every 300 messages (3 requests of 100 each, 30 of 10, etc.) if self.wait_time is None: self.wait_time = 10 if self.limit > 300 else 0 async def _load_next_chunk(self): ids = self._ids[self._offset:self._offset + _MAX_CHUNK_SIZE] if not ids: raise StopAsyncIteration self._offset += _MAX_CHUNK_SIZE from_id = None # By default, no need to validate from_id if self._ty == helpers._EntityType.CHANNEL: try: r = await self.client( functions.channels.GetMessagesRequest(self._entity, ids)) except errors.MessageIdsEmptyError: # All IDs were invalid, use a dummy result r = types.messages.MessagesNotModified(len(ids)) else: r = await self.client(functions.messages.GetMessagesRequest(ids)) if self._entity: from_id = await self.client._get_peer(self._entity) if isinstance(r, types.messages.MessagesNotModified): self.buffer.extend(None for _ in ids) return entities = {utils.get_peer_id(x): x for x in itertools.chain(r.users, r.chats)} # Telegram seems to return the messages in the order in which # we asked them for, so we don't need to check it ourselves, # unless some messages were invalid in which case Telegram # may decide to not send them at all. # # The passed message IDs may not belong to the desired entity # since the user can enter arbitrary numbers which can belong to # arbitrary chats. Validate these unless ``from_id is None``. for message in r.messages: if isinstance(message, types.MessageEmpty) or ( from_id and message.peer_id != from_id): self.buffer.append(None) else: message._finish_init(self.client, entities, self._entity) self.buffer.append(message) class MessageMethods: # region Public methods # region Message retrieval def iter_messages( self: 'TelegramClient', entity: 'hints.EntityLike', limit: float = None, *, offset_date: 'hints.DateLike' = None, offset_id: int = 0, max_id: int = 0, min_id: int = 0, add_offset: int = 0, search: str = None, filter: 'typing.Union[types.TypeMessagesFilter, typing.Type[types.TypeMessagesFilter]]' = None, from_user: 'hints.EntityLike' = None, wait_time: float = None, ids: 'typing.Union[int, typing.Sequence[int]]' = None, reverse: bool = False, reply_to: int = None, scheduled: bool = False ) -> 'typing.Union[_MessagesIter, _IDsIter]': """ Iterator over the messages for the given chat. The default order is from newest to oldest, but this behaviour can be changed with the `reverse` parameter. If either `search`, `filter` or `from_user` are provided, :tl:`messages.Search` will be used instead of :tl:`messages.getHistory`. .. note:: Telegram's flood wait limit for :tl:`GetHistoryRequest` seems to be around 30 seconds per 10 requests, therefore a sleep of 1 second is the default for this limit (or above). Arguments entity (`entity`): The entity from whom to retrieve the message history. It may be `None` to perform a global search, or to get messages by their ID from no particular chat. Note that some of the offsets will not work if this is the case. Note that if you want to perform a global search, you **must** set a non-empty `search` string, a `filter`. or `from_user`. limit (`int` | `None`, optional): 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. offset_date (`datetime`): Offset date (messages *previous* to this date will be retrieved). Exclusive. offset_id (`int`): Offset message ID (only messages *previous* to the given ID will be retrieved). Exclusive. max_id (`int`): All the messages with a higher (newer) ID or equal to this will be excluded. min_id (`int`): All the messages with a lower (older) ID or equal to this will be excluded. add_offset (`int`): Additional message offset (all of the specified offsets + this offset = older messages). search (`str`): The string to be used as a search query. filter (:tl:`MessagesFilter` | `type`): The filter to use when returning messages. For instance, :tl:`InputMessagesFilterPhotos` would yield only messages containing photos. from_user (`entity`): Only messages from this entity will be returned. wait_time (`int`): Wait time (in seconds) between different :tl:`GetHistoryRequest`. Use this parameter to avoid hitting the ``FloodWaitError`` as needed. If left to `None`, it will default to 1 second only if the limit is higher than 3000. If the ``ids`` parameter is used, this time will default to 10 seconds only if the amount of IDs is higher than 300. ids (`int`, `list`): A single integer ID (or several IDs) for the message that should be returned. This parameter takes precedence over the rest (which will be ignored if this is set). This can for instance be used to get the message with ID 123 from a channel. Note that if the message doesn't exist, `None` will appear in its place, so that zipping the list of IDs with the messages can match one-to-one. .. note:: At the time of writing, Telegram will **not** return :tl:`MessageEmpty` for :tl:`InputMessageReplyTo` IDs that failed (i.e. the message is not replying to any, or is replying to a deleted message). This means that it is **not** possible to match messages one-by-one, so be careful if you use non-integers in this parameter. reverse (`bool`, optional): If set to `True`, the messages will be returned in reverse order (from oldest to newest, instead of the default newest to oldest). This also means that the meaning of `offset_id` and `offset_date` parameters is reversed, although they will still be exclusive. `min_id` becomes equivalent to `offset_id` instead of being `max_id` as well since messages are returned in ascending order. You cannot use this if both `entity` and `ids` are `None`. reply_to (`int`, optional): If set to a message ID, the messages that reply to this ID will be returned. This feature is also known as comments in posts of broadcast channels, or viewing threads in groups. This feature can only be used in broadcast channels and their linked megagroups. Using it in a chat or private conversation will result in ``telethon.errors.PeerIdInvalidError`` to occur. When using this parameter, the ``filter`` and ``search`` parameters have no effect, since Telegram's API doesn't support searching messages in replies. .. note:: This feature is used to get replies to a message in the *discussion* group. If the same broadcast channel sends a message and replies to it itself, that reply will not be included in the results. scheduled (`bool`, optional): If set to `True`, messages which are scheduled will be returned. All other parameter will be ignored for this, except `entity`. Yields Instances of `Message `. Example .. code-block:: python # From most-recent to oldest async for message in client.iter_messages(chat): print(message.id, message.text) # From oldest to most-recent async for message in client.iter_messages(chat, reverse=True): print(message.id, message.text) # Filter by sender async for message in client.iter_messages(chat, from_user='me'): print(message.text) # Server-side search with fuzzy text async for message in client.iter_messages(chat, search='hello'): print(message.id) # Filter by message type: from telethon.tl.types import InputMessagesFilterPhotos async for message in client.iter_messages(chat, filter=InputMessagesFilterPhotos): print(message.photo) # Getting comments from a post in a channel: async for message in client.iter_messages(channel, reply_to=123): print(message.chat.title, message.text) """ if ids is not None: if not utils.is_list_like(ids): ids = [ids] return _IDsIter( client=self, reverse=reverse, wait_time=wait_time, limit=len(ids), entity=entity, ids=ids ) return _MessagesIter( client=self, reverse=reverse, wait_time=wait_time, limit=limit, entity=entity, offset_id=offset_id, min_id=min_id, max_id=max_id, from_user=from_user, offset_date=offset_date, add_offset=add_offset, filter=filter, search=search, reply_to=reply_to, scheduled=scheduled ) async def get_messages( self: 'TelegramClient', *args, **kwargs ) -> typing.Union['hints.TotalList', typing.Optional['types.Message']]: """ Same as `iter_messages()`, but returns a `TotalList ` instead. If the `limit` is not set, it will be 1 by default unless both `min_id` **and** `max_id` are set (as *named* arguments), in which case the entire range will be returned. This is so because any integer limit would be rather arbitrary and it's common to only want to fetch one message, but if a range is specified it makes sense that it should return the entirety of it. If `ids` is present in the *named* arguments and is not a list, a single `Message ` will be returned for convenience instead of a list. Example .. code-block:: python # Get 0 photos and print the total to show how many photos there are from telethon.tl.types import InputMessagesFilterPhotos photos = await client.get_messages(chat, 0, filter=InputMessagesFilterPhotos) print(photos.total) # Get all the photos photos = await client.get_messages(chat, None, filter=InputMessagesFilterPhotos) # Get messages by ID: message_1337 = await client.get_messages(chat, ids=1337) """ if len(args) == 1 and 'limit' not in kwargs: if 'min_id' in kwargs and 'max_id' in kwargs: kwargs['limit'] = None else: kwargs['limit'] = 1 it = self.iter_messages(*args, **kwargs) ids = kwargs.get('ids') if ids and not utils.is_list_like(ids): async for message in it: return message else: # Iterator exhausted = empty, to handle InputMessageReplyTo return None return await it.collect() get_messages.__signature__ = inspect.signature(iter_messages) # endregion # region Message sending/editing/deleting async def _get_comment_data( self: 'TelegramClient', entity: 'hints.EntityLike', message: 'typing.Union[int, types.Message]' ): r = await self(functions.messages.GetDiscussionMessageRequest( peer=entity, msg_id=utils.get_message_id(message) )) m = min(r.messages, key=lambda msg: msg.id) chat = next(c for c in r.chats if c.id == m.peer_id.channel_id) return utils.get_input_peer(chat), m.id async def send_message( self: 'TelegramClient', entity: 'hints.EntityLike', message: 'hints.MessageLike' = '', *, reply_to: 'typing.Union[int, types.Message]' = None, attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None, parse_mode: typing.Optional[str] = (), formatting_entities: typing.Optional[typing.List[types.TypeMessageEntity]] = None, link_preview: bool = True, file: 'typing.Union[hints.FileLike, typing.Sequence[hints.FileLike]]' = None, thumb: 'hints.FileLike' = None, force_document: bool = False, clear_draft: bool = False, buttons: typing.Optional['hints.MarkupLike'] = None, silent: bool = None, background: bool = None, supports_streaming: bool = False, schedule: 'hints.DateLike' = None, comment_to: 'typing.Union[int, types.Message]' = None, nosound_video: bool = None, ) -> 'types.Message': """ Sends a message to the specified user, chat or channel. The default parse mode is the same as the official applications (a custom flavour of markdown). ``**bold**, `code` or __italic__`` are available. In addition you can send ``[links](https://example.com)`` and ``[mentions](@username)`` (or using IDs like in the Bot API: ``[mention](tg://user?id=123456789)``) and ``pre`` blocks with three backticks. Sending a ``/start`` command with a parameter (like ``?start=data``) is also done through this method. Simply send ``'/start data'`` to the bot. See also `Message.respond() ` and `Message.reply() `. Arguments entity (`entity`): To who will it be sent. message (`str` | `Message `): The message to be sent, or another message object to resend. The maximum length for a message is 35,000 bytes or 4,096 characters. Longer messages will not be sliced automatically, and you should slice them manually if the text to send is longer than said length. reply_to (`int` | `Message `, optional): Whether to reply to a message or not. If an integer is provided, it should be the ID of the message that it should reply to. attributes (`list`, optional): Optional attributes that override the inferred ones, like :tl:`DocumentAttributeFilename` and so on. parse_mode (`object`, optional): See the `TelegramClient.parse_mode ` property for allowed values. Markdown parsing will be used by default. formatting_entities (`list`, optional): A list of message formatting entities. When provided, the ``parse_mode`` is ignored. link_preview (`bool`, optional): Should the link preview be shown? file (`file`, optional): Sends a message with a file attached (e.g. a photo, video, audio or document). The ``message`` may be empty. thumb (`str` | `bytes` | `file`, optional): Optional JPEG thumbnail (for documents). **Telegram will ignore this parameter** unless you pass a ``.jpg`` file! The file must also be small in dimensions and in disk size. Successful thumbnails were files below 20kB and 320x320px. Width/height and dimensions/size ratios may be important. For Telegram to accept a thumbnail, you must provide the dimensions of the underlying media through ``attributes=`` with :tl:`DocumentAttributesVideo` or by installing the optional ``hachoir`` dependency. force_document (`bool`, optional): Whether to send the given file as a document or not. clear_draft (`bool`, optional): Whether the existing draft should be cleared or not. buttons (`list`, `custom.Button `, :tl:`KeyboardButton`): The matrix (list of lists), row list or button to be shown after sending the message. This parameter will only work if you have signed in as a bot. You can also pass your own :tl:`ReplyMarkup` here. All the following limits apply together: * There can be 100 buttons at most (any more are ignored). * There can be 8 buttons per row at most (more are ignored). * The maximum callback data per button is 64 bytes. * The maximum data that can be embedded in total is just over 4KB, shared between inline callback data and text. silent (`bool`, optional): Whether the message should notify people in a broadcast channel or not. Defaults to `False`, which means it will notify them. Set it to `True` to alter this behaviour. background (`bool`, optional): Whether the message should be send in background. supports_streaming (`bool`, optional): Whether the sent video supports streaming or not. Note that Telegram only recognizes as streamable some formats like MP4, and others like AVI or MKV will not work. You should convert these to MP4 before sending if you want them to be streamable. Unsupported formats will result in ``VideoContentTypeError``. schedule (`hints.DateLike`, optional): If set, the message won't send immediately, and instead it will be scheduled to be automatically sent at a later time. comment_to (`int` | `Message `, optional): Similar to ``reply_to``, but replies in the linked group of a broadcast channel instead (effectively leaving a "comment to" the specified message). This parameter takes precedence over ``reply_to``. If there is no linked chat, `telethon.errors.sgIdInvalidError` is raised. nosound_video (`bool`, optional): Only applicable when sending a video file without an audio track. If set to ``True``, the video will be displayed in Telegram as a video. If set to ``False``, Telegram will attempt to display the video as an animated gif. (It may still display as a video due to other factors.) The value is ignored if set on non-video files. This is set to ``True`` for albums, as gifs cannot be sent in albums. Returns The sent `custom.Message `. Example .. code-block:: python # Markdown is the default await client.send_message('me', 'Hello **world**!') # Default to another parse mode client.parse_mode = 'html' await client.send_message('me', 'Some bold and italic text') await client.send_message('me', 'An URL') # code and pre tags also work, but those break the documentation :) await client.send_message('me', 'Mentions') # Explicit parse mode # No parse mode by default client.parse_mode = None # ...but here I want markdown await client.send_message('me', 'Hello, **world**!', parse_mode='md') # ...and here I need HTML await client.send_message('me', 'Hello, world!', parse_mode='html') # If you logged in as a bot account, you can send buttons from telethon import events, Button @client.on(events.CallbackQuery) async def callback(event): await event.edit('Thank you for clicking {}!'.format(event.data)) # Single inline button await client.send_message(chat, 'A single button, with "clk1" as data', buttons=Button.inline('Click me', b'clk1')) # Matrix of inline buttons await client.send_message(chat, 'Pick one from this grid', buttons=[ [Button.inline('Left'), Button.inline('Right')], [Button.url('Check this site!', 'https://example.com')] ]) # Reply keyboard await client.send_message(chat, 'Welcome', buttons=[ Button.text('Thanks!', resize=True, single_use=True), Button.request_phone('Send phone'), Button.request_location('Send location') ]) # Forcing replies or clearing buttons. await client.send_message(chat, 'Reply to me', buttons=Button.force_reply()) await client.send_message(chat, 'Bye Keyboard!', buttons=Button.clear()) # Scheduling a message to be sent after 5 minutes from datetime import timedelta await client.send_message(chat, 'Hi, future!', schedule=timedelta(minutes=5)) """ if file is not None: if isinstance(message, types.Message): formatting_entities = formatting_entities or message.entities message = message.message return await self.send_file( entity, file, caption=message, reply_to=reply_to, attributes=attributes, parse_mode=parse_mode, force_document=force_document, thumb=thumb, buttons=buttons, clear_draft=clear_draft, silent=silent, schedule=schedule, supports_streaming=supports_streaming, formatting_entities=formatting_entities, comment_to=comment_to, background=background, nosound_video=nosound_video, ) entity = await self.get_input_entity(entity) if comment_to is not None: entity, reply_to = await self._get_comment_data(entity, comment_to) else: reply_to = utils.get_message_id(reply_to) if isinstance(message, types.Message): if buttons is None: markup = message.reply_markup else: markup = self.build_reply_markup(buttons) if silent is None: silent = message.silent if (message.media and not isinstance( message.media, types.MessageMediaWebPage)): return await self.send_file( entity, message.media, caption=message.message, silent=silent, background=background, reply_to=reply_to, buttons=markup, formatting_entities=message.entities, parse_mode=None, # explicitly disable parse_mode to force using even empty formatting_entities schedule=schedule ) request = functions.messages.SendMessageRequest( peer=entity, message=message.message or '', silent=silent, background=background, reply_to=None if reply_to is None else types.InputReplyToMessage(reply_to), reply_markup=markup, entities=message.entities, clear_draft=clear_draft, no_webpage=not isinstance( message.media, types.MessageMediaWebPage), schedule_date=schedule ) message = message.message else: if formatting_entities is None: message, formatting_entities = await self._parse_message_text(message, parse_mode) if not message: raise ValueError( 'The message cannot be empty unless a file is provided' ) request = functions.messages.SendMessageRequest( peer=entity, message=message, entities=formatting_entities, no_webpage=not link_preview, reply_to=None if reply_to is None else types.InputReplyToMessage(reply_to), clear_draft=clear_draft, silent=silent, background=background, reply_markup=self.build_reply_markup(buttons), schedule_date=schedule ) result = await self(request) if isinstance(result, types.UpdateShortSentMessage): message = types.Message( id=result.id, peer_id=await self._get_peer(entity), message=message, date=result.date, out=result.out, media=result.media, entities=result.entities, reply_markup=request.reply_markup, ttl_period=result.ttl_period, reply_to=request.reply_to ) message._finish_init(self, {}, entity) return message return self._get_response_message(request, result, entity) async def forward_messages( self: 'TelegramClient', entity: 'hints.EntityLike', messages: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]', from_peer: 'hints.EntityLike' = None, *, background: bool = None, with_my_score: bool = None, silent: bool = None, as_album: bool = None, schedule: 'hints.DateLike' = None, drop_author: bool = None, drop_media_captions: bool = None, ) -> 'typing.Sequence[types.Message]': """ Forwards the given messages to the specified entity. If you want to "forward" a message without the forward header (the "forwarded from" text), you should use `send_message` with the original message instead. This will send a copy of it. See also `Message.forward_to() `. Arguments entity (`entity`): To which entity the message(s) will be forwarded. messages (`list` | `int` | `Message `): The message(s) to forward, or their integer IDs. from_peer (`entity`): If the given messages are integer IDs and not instances of the ``Message`` class, this *must* be specified in order for the forward to work. This parameter indicates the entity from which the messages should be forwarded. silent (`bool`, optional): Whether the message should notify people with sound or not. Defaults to `False` (send with a notification sound unless the person has the chat muted). Set it to `True` to alter this behaviour. background (`bool`, optional): Whether the message should be forwarded in background. with_my_score (`bool`, optional): Whether forwarded should contain your game score. as_album (`bool`, optional): This flag no longer has any effect. schedule (`hints.DateLike`, optional): If set, the message(s) won't forward immediately, and instead they will be scheduled to be automatically sent at a later time. drop_author (`bool`, optional): Whether to forward messages without quoting the original author. drop_media_captions (`bool`, optional): Whether to strip captions from media. Setting this to `True` requires that `drop_author` also be set to `True`. Returns The list of forwarded `Message `, or a single one if a list wasn't provided as input. Note that if all messages are invalid (i.e. deleted) the call will fail with ``MessageIdInvalidError``. If only some are invalid, the list will have `None` instead of those messages. Example .. code-block:: python # a single one await client.forward_messages(chat, message) # or await client.forward_messages(chat, message_id, from_chat) # or await message.forward_to(chat) # multiple await client.forward_messages(chat, messages) # or await client.forward_messages(chat, message_ids, from_chat) # Forwarding as a copy await client.send_message(chat, message) """ if as_album is not None: warnings.warn('the as_album argument is deprecated and no longer has any effect') single = not utils.is_list_like(messages) if single: messages = (messages,) entity = await self.get_input_entity(entity) if from_peer: from_peer = await self.get_input_entity(from_peer) from_peer_id = await self.get_peer_id(from_peer) else: from_peer_id = None def get_key(m): if isinstance(m, int): if from_peer_id is not None: return from_peer_id raise ValueError('from_peer must be given if integer IDs are used') elif isinstance(m, types.Message): return m.chat_id else: raise TypeError('Cannot forward messages of type {}'.format(type(m))) sent = [] for _chat_id, chunk in itertools.groupby(messages, key=get_key): chunk = list(chunk) if isinstance(chunk[0], int): chat = from_peer else: chat = from_peer or await self.get_input_entity(chunk[0].peer_id) chunk = [m.id for m in chunk] req = functions.messages.ForwardMessagesRequest( from_peer=chat, id=chunk, to_peer=entity, silent=silent, background=background, with_my_score=with_my_score, schedule_date=schedule, drop_author=drop_author, drop_media_captions=drop_media_captions ) result = await self(req) sent.extend(self._get_response_message(req, result, entity)) return sent[0] if single else sent async def edit_message( self: 'TelegramClient', entity: 'typing.Union[hints.EntityLike, types.Message]', message: 'typing.Union[int, types.Message, types.InputMessageID, str]' = None, text: str = None, *, parse_mode: str = (), attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None, formatting_entities: typing.Optional[typing.List[types.TypeMessageEntity]] = None, link_preview: bool = True, file: 'hints.FileLike' = None, thumb: 'hints.FileLike' = None, force_document: bool = False, buttons: typing.Optional['hints.MarkupLike'] = None, supports_streaming: bool = False, schedule: 'hints.DateLike' = None ) -> 'types.Message': """ Edits the given message to change its text or media. See also `Message.edit() `. Arguments entity (`entity` | `Message `): From which chat to edit the message. This can also be the message to be edited, and the entity will be inferred from it, so the next parameter will be assumed to be the message text. You may also pass a :tl:`InputBotInlineMessageID` or :tl:`InputBotInlineMessageID64`, which is the only way to edit messages that were sent after the user selects an inline query result. message (`int` | `Message ` | :tl:`InputMessageID` | `str`): The ID of the message (or `Message ` itself) to be edited. If the `entity` was a `Message `, then this message will be treated as the new text. text (`str`, optional): The new text of the message. Does nothing if the `entity` was a `Message `. parse_mode (`object`, optional): See the `TelegramClient.parse_mode ` property for allowed values. Markdown parsing will be used by default. attributes (`list`, optional): Optional attributes that override the inferred ones, like :tl:`DocumentAttributeFilename` and so on. formatting_entities (`list`, optional): A list of message formatting entities. When provided, the ``parse_mode`` is ignored. link_preview (`bool`, optional): Should the link preview be shown? file (`str` | `bytes` | `file` | `media`, optional): The file object that should replace the existing media in the message. thumb (`str` | `bytes` | `file`, optional): Optional JPEG thumbnail (for documents). **Telegram will ignore this parameter** unless you pass a ``.jpg`` file! The file must also be small in dimensions and in disk size. Successful thumbnails were files below 20kB and 320x320px. Width/height and dimensions/size ratios may be important. For Telegram to accept a thumbnail, you must provide the dimensions of the underlying media through ``attributes=`` with :tl:`DocumentAttributesVideo` or by installing the optional ``hachoir`` dependency. force_document (`bool`, optional): Whether to send the given file as a document or not. buttons (`list`, `custom.Button `, :tl:`KeyboardButton`): The matrix (list of lists), row list or button to be shown after sending the message. This parameter will only work if you have signed in as a bot. You can also pass your own :tl:`ReplyMarkup` here. supports_streaming (`bool`, optional): Whether the sent video supports streaming or not. Note that Telegram only recognizes as streamable some formats like MP4, and others like AVI or MKV will not work. You should convert these to MP4 before sending if you want them to be streamable. Unsupported formats will result in ``VideoContentTypeError``. schedule (`hints.DateLike`, optional): If set, the message won't be edited immediately, and instead it will be scheduled to be automatically edited at a later time. Note that this parameter will have no effect if you are trying to edit a message that was sent via inline bots. Returns The edited `Message `, unless `entity` was a :tl:`InputBotInlineMessageID` or :tl:`InputBotInlineMessageID64` in which case this method returns a boolean. Raises ``MessageAuthorRequiredError`` if you're not the author of the message but tried editing it anyway. ``MessageNotModifiedError`` if the contents of the message were not modified at all. ``MessageIdInvalidError`` if the ID of the message is invalid (the ID itself may be correct, but the message with that ID cannot be edited). For example, when trying to edit messages with a reply markup (or clear markup) this error will be raised. Example .. code-block:: python message = await client.send_message(chat, 'hello') await client.edit_message(chat, message, 'hello!') # or await client.edit_message(chat, message.id, 'hello!!') # or await client.edit_message(message, 'hello!!!') """ if isinstance(entity, (types.InputBotInlineMessageID, types.InputBotInlineMessageID64)): text = text or message message = entity elif isinstance(entity, types.Message): text = message # Shift the parameters to the right message = entity entity = entity.peer_id if formatting_entities is None: text, formatting_entities = await self._parse_message_text(text, parse_mode) file_handle, media, image = await self._file_to_media(file, supports_streaming=supports_streaming, thumb=thumb, attributes=attributes, force_document=force_document) if isinstance(entity, (types.InputBotInlineMessageID, types.InputBotInlineMessageID64)): request = functions.messages.EditInlineBotMessageRequest( id=entity, message=text, no_webpage=not link_preview, entities=formatting_entities, media=media, reply_markup=self.build_reply_markup(buttons) ) # Invoke `messages.editInlineBotMessage` from the right datacenter. # Otherwise, Telegram will error with `MESSAGE_ID_INVALID` and do nothing. exported = self.session.dc_id != entity.dc_id if exported: try: sender = await self._borrow_exported_sender(entity.dc_id) return await self._call(sender, request) finally: await self._return_exported_sender(sender) else: return await self(request) entity = await self.get_input_entity(entity) request = functions.messages.EditMessageRequest( peer=entity, id=utils.get_message_id(message), message=text, no_webpage=not link_preview, entities=formatting_entities, media=media, reply_markup=self.build_reply_markup(buttons), schedule_date=schedule ) msg = self._get_response_message(request, await self(request), entity) return msg async def delete_messages( self: 'TelegramClient', entity: 'hints.EntityLike', message_ids: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]', *, revoke: bool = True) -> 'typing.Sequence[types.messages.AffectedMessages]': """ Deletes the given messages, optionally "for everyone". See also `Message.delete() `. .. warning:: This method does **not** validate that the message IDs belong to the chat that you passed! It's possible for the method to delete messages from different private chats and small group chats at once, so make sure to pass the right IDs. Arguments entity (`entity`): From who the message will be deleted. This can actually be `None` for normal chats, but **must** be present for channels and megagroups. message_ids (`list` | `int` | `Message `): The IDs (or ID) or messages to be deleted. revoke (`bool`, optional): Whether the message should be deleted for everyone or not. By default it has the opposite behaviour of official clients, and it will delete the message for everyone. `Since 24 March 2019 `_, you can also revoke messages of any age (i.e. messages sent long in the past) the *other* person sent in private conversations (and of course your messages too). Disabling this has no effect on channels or megagroups, since it will unconditionally delete the message for everyone. Returns A list of :tl:`AffectedMessages`, each item being the result for the delete calls of the messages in chunks of 100 each. Example .. code-block:: python await client.delete_messages(chat, messages) """ if not utils.is_list_like(message_ids): message_ids = (message_ids,) message_ids = ( m.id if isinstance(m, ( types.Message, types.MessageService, types.MessageEmpty)) else int(m) for m in message_ids ) if entity: entity = await self.get_input_entity(entity) ty = helpers._entity_type(entity) else: # no entity (None), set a value that's not a channel for private delete ty = helpers._EntityType.USER if ty == helpers._EntityType.CHANNEL: return await self([functions.channels.DeleteMessagesRequest( entity, list(c)) for c in utils.chunks(message_ids)]) else: return await self([functions.messages.DeleteMessagesRequest( list(c), revoke) for c in utils.chunks(message_ids)]) # endregion # region Miscellaneous async def send_read_acknowledge( self: 'TelegramClient', entity: 'hints.EntityLike', message: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]' = None, *, max_id: int = None, clear_mentions: bool = False, clear_reactions: bool = False) -> bool: """ Marks messages as read and optionally clears mentions. This effectively marks a message as read (or more than one) in the given conversation. If neither message nor maximum ID are provided, all messages will be marked as read by assuming that ``max_id = 0``. If a message or maximum ID is provided, all the messages up to and including such ID will be marked as read (for all messages whose ID ≤ max_id). See also `Message.mark_read() `. Arguments entity (`entity`): The chat where these messages are located. message (`list` | `Message `): Either a list of messages or a single message. max_id (`int`): Until which message should the read acknowledge be sent for. This has priority over the ``message`` parameter. clear_mentions (`bool`): Whether the mention badge should be cleared (so that there are no more mentions) or not for the given entity. If no message is provided, this will be the only action taken. clear_reactions (`bool`): Whether the reactions badge should be cleared (so that there are no more reaction notifications) or not for the given entity. If no message is provided, this will be the only action taken. Example .. code-block:: python # using a Message object await client.send_read_acknowledge(chat, message) # ...or using the int ID of a Message await client.send_read_acknowledge(chat, message_id) # ...or passing a list of messages to mark as read await client.send_read_acknowledge(chat, messages) """ if max_id is None: if not message: max_id = 0 else: if utils.is_list_like(message): max_id = max(msg.id for msg in message) else: max_id = message.id entity = await self.get_input_entity(entity) if clear_mentions: await self(functions.messages.ReadMentionsRequest(entity)) if max_id is None and not clear_reactions: return True if clear_reactions: await self(functions.messages.ReadReactionsRequest(entity)) if max_id is None: return True if max_id is not None: if helpers._entity_type(entity) == helpers._EntityType.CHANNEL: return await self(functions.channels.ReadHistoryRequest( utils.get_input_channel(entity), max_id=max_id)) else: return await self(functions.messages.ReadHistoryRequest( entity, max_id=max_id)) return False async def pin_message( self: 'TelegramClient', entity: 'hints.EntityLike', message: 'typing.Optional[hints.MessageIDLike]', *, notify: bool = False, pm_oneside: bool = False ): """ Pins a message in a chat. The default behaviour is to *not* notify members, unlike the official applications. See also `Message.pin() `. Arguments entity (`entity`): The chat where the message should be pinned. message (`int` | `Message `): The message or the message ID to pin. If it's `None`, all messages will be unpinned instead. notify (`bool`, optional): Whether the pin should notify people or not. pm_oneside (`bool`, optional): Whether the message should be pinned for everyone or not. By default it has the opposite behaviour of official clients, and it will pin the message for both sides, in private chats. Example .. code-block:: python # Send and pin a message to annoy everyone message = await client.send_message(chat, 'Pinotifying is fun!') await client.pin_message(chat, message, notify=True) """ return await self._pin(entity, message, unpin=False, notify=notify, pm_oneside=pm_oneside) async def unpin_message( self: 'TelegramClient', entity: 'hints.EntityLike', message: 'typing.Optional[hints.MessageIDLike]' = None, *, notify: bool = False ): """ Unpins a message in a chat. If no message ID is specified, all pinned messages will be unpinned. See also `Message.unpin() `. Arguments entity (`entity`): The chat where the message should be pinned. message (`int` | `Message `): The message or the message ID to unpin. If it's `None`, all messages will be unpinned instead. Example .. code-block:: python # Unpin all messages from a chat await client.unpin_message(chat) """ return await self._pin(entity, message, unpin=True, notify=notify) async def _pin(self, entity, message, *, unpin, notify=False, pm_oneside=False): message = utils.get_message_id(message) or 0 entity = await self.get_input_entity(entity) if message <= 0: # old behaviour accepted negative IDs to unpin await self(functions.messages.UnpinAllMessagesRequest(entity)) return request = functions.messages.UpdatePinnedMessageRequest( peer=entity, id=message, silent=not notify, unpin=unpin, pm_oneside=pm_oneside ) result = await self(request) # Unpinning does not produce a service message. # Pinning a message that was already pinned also produces no service message. # Pinning a message in your own chat does not produce a service message, # but pinning on a private conversation with someone else does. if unpin or not result.updates: return # Pinning a message that doesn't exist would RPC-error earlier return self._get_response_message(request, result, entity) # endregion # endregion Telethon-1.39.0/telethon/client/telegrambaseclient.py000066400000000000000000001145671475566265000227300ustar00rootroot00000000000000import abc import re import asyncio import collections import logging import platform import time import typing import datetime import pathlib from .. import version, helpers, __name__ as __base_name__ from ..crypto import rsa from ..extensions import markdown from ..network import MTProtoSender, Connection, ConnectionTcpFull, TcpMTProxy from ..sessions import Session, SQLiteSession, MemorySession from ..tl import functions, types from ..tl.alltlobjects import LAYER from .._updates import MessageBox, EntityCache as MbEntityCache, SessionState, ChannelState, Entity, EntityType DEFAULT_DC_ID = 2 DEFAULT_IPV4_IP = '149.154.167.51' DEFAULT_IPV6_IP = '2001:67c:4e8:f002::a' DEFAULT_PORT = 443 if typing.TYPE_CHECKING: from .telegramclient import TelegramClient _base_log = logging.getLogger(__base_name__) # In seconds, how long to wait before disconnecting a exported sender. _DISCONNECT_EXPORTED_AFTER = 60 class _ExportState: def __init__(self): # ``n`` is the amount of borrows a given sender has; # once ``n`` reaches ``0``, disconnect the sender after a while. self._n = 0 self._zero_ts = 0 self._connected = False def add_borrow(self): self._n += 1 self._connected = True def add_return(self): self._n -= 1 assert self._n >= 0, 'returned sender more than it was borrowed' if self._n == 0: self._zero_ts = time.time() def should_disconnect(self): return (self._n == 0 and self._connected and (time.time() - self._zero_ts) > _DISCONNECT_EXPORTED_AFTER) def need_connect(self): return not self._connected def mark_disconnected(self): assert self.should_disconnect(), 'marked as disconnected when it was borrowed' self._connected = False # TODO How hard would it be to support both `trio` and `asyncio`? class TelegramBaseClient(abc.ABC): """ This is the abstract base class for the client. It defines some basic stuff like connecting, switching data center, etc, and leaves the `__call__` unimplemented. Arguments session (`str` | `telethon.sessions.abstract.Session`, `None`): The file name of the session file to be used if a string is given (it may be a full path), or the Session instance to be used otherwise. If it's `None`, the session will not be saved, and you should call :meth:`.log_out()` when you're done. Note that if you pass a string it will be a file in the current working directory, although you can also pass absolute paths. The session file contains enough information for you to login without re-sending the code, so if you have to enter the code more than once, maybe you're changing the working directory, renaming or removing the file, or using random names. api_id (`int` | `str`): The API ID you obtained from https://my.telegram.org. api_hash (`str`): The API hash you obtained from https://my.telegram.org. connection (`telethon.network.connection.common.Connection`, optional): The connection instance to be used when creating a new connection to the servers. It **must** be a type. Defaults to `telethon.network.connection.tcpfull.ConnectionTcpFull`. use_ipv6 (`bool`, optional): Whether to connect to the servers through IPv6 or not. By default this is `False` as IPv6 support is not too widespread yet. proxy (`tuple` | `list` | `dict`, optional): An iterable consisting of the proxy info. If `connection` is one of `MTProxy`, then it should contain MTProxy credentials: ``('hostname', port, 'secret')``. Otherwise, it's meant to store function parameters for PySocks, like ``(type, 'hostname', port)``. See https://github.com/Anorov/PySocks#usage-1 for more. local_addr (`str` | `tuple`, optional): Local host address (and port, optionally) used to bind the socket to locally. You only need to use this if you have multiple network cards and want to use a specific one. timeout (`int` | `float`, optional): The timeout in seconds to be used when connecting. This is **not** the timeout to be used when ``await``'ing for invoked requests, and you should use ``asyncio.wait`` or ``asyncio.wait_for`` for that. request_retries (`int` | `None`, optional): How many times a request should be retried. Request are retried when Telegram is having internal issues (due to either ``errors.ServerError`` or ``errors.RpcCallFailError``), when there is a ``errors.FloodWaitError`` less than `flood_sleep_threshold`, or when there's a migrate error. May take a negative or `None` value for infinite retries, but this is not recommended, since some requests can always trigger a call fail (such as searching for messages). connection_retries (`int` | `None`, optional): How many times the reconnection should retry, either on the initial connection or when Telegram disconnects us. May be set to a negative or `None` value for infinite retries, but this is not recommended, since the program can get stuck in an infinite loop. retry_delay (`int` | `float`, optional): The delay in seconds to sleep between automatic reconnections. auto_reconnect (`bool`, optional): Whether reconnection should be retried `connection_retries` times automatically if Telegram disconnects us or not. sequential_updates (`bool`, optional): By default every incoming update will create a new task, so you can handle several updates in parallel. Some scripts need the order in which updates are processed to be sequential, and this setting allows them to do so. If set to `True`, incoming updates will be put in a queue and processed sequentially. This means your event handlers should *not* perform long-running operations since new updates are put inside of an unbounded queue. flood_sleep_threshold (`int` | `float`, optional): The threshold below which the library should automatically sleep on flood wait and slow mode wait errors (inclusive). For instance, if a ``FloodWaitError`` for 17s occurs and `flood_sleep_threshold` is 20s, the library will ``sleep`` automatically. If the error was for 21s, it would ``raise FloodWaitError`` instead. Values larger than a day (like ``float('inf')``) will be changed to a day. raise_last_call_error (`bool`, optional): When API calls fail in a way that causes Telethon to retry automatically, should the RPC error of the last attempt be raised instead of a generic ValueError. This is mostly useful for detecting when Telegram has internal issues. device_model (`str`, optional): "Device model" to be sent when creating the initial connection. Defaults to 'PC (n)bit' derived from ``platform.uname().machine``, or its direct value if unknown. system_version (`str`, optional): "System version" to be sent when creating the initial connection. Defaults to ``platform.uname().release`` stripped of everything ahead of -. app_version (`str`, optional): "App version" to be sent when creating the initial connection. Defaults to `telethon.version.__version__`. lang_code (`str`, optional): "Language code" to be sent when creating the initial connection. Defaults to ``'en'``. system_lang_code (`str`, optional): "System lang code" to be sent when creating the initial connection. Defaults to `lang_code`. loop (`asyncio.AbstractEventLoop`, optional): Asyncio event loop to use. Defaults to `asyncio.get_running_loop()`. This argument is ignored. base_logger (`str` | `logging.Logger`, optional): Base logger name or instance to use. If a `str` is given, it'll be passed to `logging.getLogger()`. If a `logging.Logger` is given, it'll be used directly. If something else or nothing is given, the default logger will be used. receive_updates (`bool`, optional): Whether the client will receive updates or not. By default, updates will be received from Telegram as they occur. Turning this off means that Telegram will not send updates at all so event handlers, conversations, and QR login will not work. However, certain scripts don't need updates, so this will reduce the amount of bandwidth used. entity_cache_limit (`int`, optional): How many users, chats and channels to keep in the in-memory cache at most. This limit is checked against when processing updates. When this limit is reached or exceeded, all entities that are not required for update handling will be flushed to the session file. Note that this implies that there is a lower bound to the amount of entities that must be kept in memory. Setting this limit too low will cause the library to attempt to flush entities to the session file even if no entities can be removed from the in-memory cache, which will degrade performance. """ # Current TelegramClient version __version__ = version.__version__ # Cached server configuration (with .dc_options), can be "global" _config = None _cdn_config = None # region Initialization def __init__( self: 'TelegramClient', session: 'typing.Union[str, pathlib.Path, Session]', api_id: int, api_hash: str, *, connection: 'typing.Type[Connection]' = ConnectionTcpFull, use_ipv6: bool = False, proxy: typing.Union[tuple, dict] = None, local_addr: typing.Union[str, tuple] = None, timeout: int = 10, request_retries: int = 5, connection_retries: int = 5, retry_delay: int = 1, auto_reconnect: bool = True, sequential_updates: bool = False, flood_sleep_threshold: int = 60, raise_last_call_error: bool = False, device_model: str = None, system_version: str = None, app_version: str = None, lang_code: str = 'en', system_lang_code: str = 'en', loop: asyncio.AbstractEventLoop = None, base_logger: typing.Union[str, logging.Logger] = None, receive_updates: bool = True, catch_up: bool = False, entity_cache_limit: int = 5000 ): if not api_id or not api_hash: raise ValueError( "Your API ID or Hash cannot be empty or None. " "Refer to telethon.rtfd.io for more information.") self._use_ipv6 = use_ipv6 if isinstance(base_logger, str): base_logger = logging.getLogger(base_logger) elif not isinstance(base_logger, logging.Logger): base_logger = _base_log class _Loggers(dict): def __missing__(self, key): if key.startswith("telethon."): key = key.split('.', maxsplit=1)[1] return base_logger.getChild(key) self._log = _Loggers() # Determine what session object we have if isinstance(session, (str, pathlib.Path)): try: session = SQLiteSession(str(session)) except ImportError: import warnings warnings.warn( 'The sqlite3 module is not available under this ' 'Python installation and no custom session ' 'instance was given; using MemorySession.\n' 'You will need to re-login every time unless ' 'you use another session storage' ) session = MemorySession() elif session is None: session = MemorySession() elif not isinstance(session, Session): raise TypeError( 'The given session must be a str or a Session instance.' ) # ':' in session.server_address is True if it's an IPv6 address if (not session.server_address or (':' in session.server_address) != use_ipv6): session.set_dc( DEFAULT_DC_ID, DEFAULT_IPV6_IP if self._use_ipv6 else DEFAULT_IPV4_IP, DEFAULT_PORT ) session.save() self.flood_sleep_threshold = flood_sleep_threshold # TODO Use AsyncClassWrapper(session) # ChatGetter and SenderGetter can use the in-memory _mb_entity_cache # to avoid network access and the need for await in session files. # # The session files only wants the entities to persist # them to disk, and to save additional useful information. # TODO Session should probably return all cached # info of entities, not just the input versions self.session = session self.api_id = int(api_id) self.api_hash = api_hash # Current proxy implementation requires `sock_connect`, and some # event loops lack this method. If the current loop is missing it, # bail out early and suggest an alternative. # # TODO A better fix is obviously avoiding the use of `sock_connect` # # See https://github.com/LonamiWebs/Telethon/issues/1337 for details. if not callable(getattr(self.loop, 'sock_connect', None)): raise TypeError( 'Event loop of type {} lacks `sock_connect`, which is needed to use proxies.\n\n' 'Change the event loop in use to use proxies:\n' '# https://github.com/LonamiWebs/Telethon/issues/1337\n' 'import asyncio\n' 'asyncio.set_event_loop(asyncio.SelectorEventLoop())'.format( self.loop.__class__.__name__ ) ) if local_addr is not None: if use_ipv6 is False and ':' in local_addr: raise TypeError( 'A local IPv6 address must only be used with `use_ipv6=True`.' ) elif use_ipv6 is True and ':' not in local_addr: raise TypeError( '`use_ipv6=True` must only be used with a local IPv6 address.' ) self._raise_last_call_error = raise_last_call_error self._request_retries = request_retries self._connection_retries = connection_retries self._retry_delay = retry_delay or 0 self._proxy = proxy self._local_addr = local_addr self._timeout = timeout self._auto_reconnect = auto_reconnect assert isinstance(connection, type) self._connection = connection init_proxy = None if not issubclass(connection, TcpMTProxy) else \ types.InputClientProxy(*connection.address_info(proxy)) # Used on connection. Capture the variables in a lambda since # exporting clients need to create this InvokeWithLayerRequest. system = platform.uname() if system.machine in ('x86_64', 'AMD64'): default_device_model = 'PC 64bit' elif system.machine in ('i386','i686','x86'): default_device_model = 'PC 32bit' else: default_device_model = system.machine default_system_version = re.sub(r'-.+','',system.release) self._init_request = functions.InitConnectionRequest( api_id=self.api_id, device_model=device_model or default_device_model or 'Unknown', system_version=system_version or default_system_version or '1.0', app_version=app_version or self.__version__, lang_code=lang_code, system_lang_code=system_lang_code, lang_pack='', # "langPacks are for official apps only" query=None, proxy=init_proxy ) # Remember flood-waited requests to avoid making them again self._flood_waited_requests = {} # Cache ``{dc_id: (_ExportState, MTProtoSender)}`` for all borrowed senders self._borrowed_senders = {} self._borrow_sender_lock = asyncio.Lock() self._exported_sessions = {} self._loop = None # only used as a sanity check self._updates_error = None self._updates_handle = None self._keepalive_handle = None self._last_request = time.time() self._no_updates = not receive_updates # Used for non-sequential updates, in order to terminate all pending tasks on disconnect. self._sequential_updates = sequential_updates self._event_handler_tasks = set() self._authorized = None # None = unknown, False = no, True = yes # Some further state for subclasses self._event_builders = [] # {chat_id: {Conversation}} self._conversations = collections.defaultdict(set) # Hack to workaround the fact Telegram may send album updates as # different Updates when being sent from a different data center. # {grouped_id: AlbumHack} # # FIXME: We don't bother cleaning this up because it's not really # worth it, albums are pretty rare and this only holds them # for a second at most. self._albums = {} # Default parse mode self._parse_mode = markdown # Some fields to easy signing in. Let {phone: hash} be # a dictionary because the user may change their mind. self._phone_code_hash = {} self._phone = None self._tos = None # A place to store if channels are a megagroup or not (see `edit_admin`) self._megagroup_cache = {} # This is backported from v2 in a very ad-hoc way just to get proper update handling self._catch_up = catch_up self._updates_queue = asyncio.Queue() self._message_box = MessageBox(self._log['messagebox']) self._mb_entity_cache = MbEntityCache() # required for proper update handling (to know when to getDifference) self._entity_cache_limit = entity_cache_limit self._sender = MTProtoSender( self.session.auth_key, loggers=self._log, retries=self._connection_retries, delay=self._retry_delay, auto_reconnect=self._auto_reconnect, connect_timeout=self._timeout, auth_key_callback=self._auth_key_callback, updates_queue=self._updates_queue, auto_reconnect_callback=self._handle_auto_reconnect ) # endregion # region Properties @property def loop(self: 'TelegramClient') -> asyncio.AbstractEventLoop: """ Property with the ``asyncio`` event loop used by this client. Example .. code-block:: python # Download media in the background task = client.loop.create_task(message.download_media()) # Do some work ... # Join the task (wait for it to complete) await task """ return helpers.get_running_loop() @property def disconnected(self: 'TelegramClient') -> asyncio.Future: """ Property with a ``Future`` that resolves upon disconnection. Example .. code-block:: python # Wait for a disconnection to occur try: await client.disconnected except OSError: print('Error on disconnect') """ return self._sender.disconnected @property def flood_sleep_threshold(self): return self._flood_sleep_threshold @flood_sleep_threshold.setter def flood_sleep_threshold(self, value): # None -> 0, negative values don't really matter self._flood_sleep_threshold = min(value or 0, 24 * 60 * 60) # endregion # region Connecting async def connect(self: 'TelegramClient') -> None: """ Connects to Telegram. .. note:: Connect means connect and nothing else, and only one low-level request is made to notify Telegram about which layer we will be using. Before Telegram sends you updates, you need to make a high-level request, like `client.get_me() `, as described in https://core.telegram.org/api/updates. Example .. code-block:: python try: await client.connect() except OSError: print('Failed to connect') """ if self.session is None: raise ValueError('TelegramClient instance cannot be reused after logging out') if self._loop is None: self._loop = helpers.get_running_loop() elif self._loop != helpers.get_running_loop(): raise RuntimeError('The asyncio event loop must not change after connection (see the FAQ for details)') if not await self._sender.connect(self._connection( self.session.server_address, self.session.port, self.session.dc_id, loggers=self._log, proxy=self._proxy, local_addr=self._local_addr )): # We don't want to init or modify anything if we were already connected return self.session.auth_key = self._sender.auth_key self.session.save() try: # See comment when saving entities to understand this hack self_id = self.session.get_input_entity(0).access_hash self_user = self.session.get_input_entity(self_id) self._mb_entity_cache.set_self_user(self_id, None, self_user.access_hash) except ValueError: pass if self._catch_up: ss = SessionState(0, 0, False, 0, 0, 0, 0, None) cs = [] for entity_id, state in self.session.get_update_states(): if entity_id == 0: # TODO current session doesn't store self-user info but adding that is breaking on downstream session impls ss = SessionState(0, 0, False, state.pts, state.qts, int(state.date.timestamp()), state.seq, None) else: cs.append(ChannelState(entity_id, state.pts)) self._message_box.load(ss, cs) for state in cs: try: entity = self.session.get_input_entity(state.channel_id) except ValueError: self._log[__name__].warning( 'No access_hash in cache for channel %s, will not catch up', state.channel_id) else: self._mb_entity_cache.put(Entity(EntityType.CHANNEL, entity.channel_id, entity.access_hash)) self._init_request.query = functions.help.GetConfigRequest() req = self._init_request if self._no_updates: req = functions.InvokeWithoutUpdatesRequest(req) await self._sender.send(functions.InvokeWithLayerRequest(LAYER, req)) if self._message_box.is_empty(): me = await self.get_me() if me: await self._on_login(me) # also calls GetState to initialize the MessageBox self._updates_handle = self.loop.create_task(self._update_loop()) self._keepalive_handle = self.loop.create_task(self._keepalive_loop()) def is_connected(self: 'TelegramClient') -> bool: """ Returns `True` if the user has connected. This method is **not** asynchronous (don't use ``await`` on it). Example .. code-block:: python while client.is_connected(): await asyncio.sleep(1) """ sender = getattr(self, '_sender', None) return sender and sender.is_connected() def disconnect(self: 'TelegramClient'): """ Disconnects from Telegram. If the event loop is already running, this method returns a coroutine that you should await on your own code; otherwise the loop is ran until said coroutine completes. Event handlers which are currently running will be cancelled before this function returns (in order to properly clean-up their tasks). In particular, this means that using ``disconnect`` in a handler will cause code after the ``disconnect`` to never run. If this is needed, consider spawning a separate task to do the remaining work. Example .. code-block:: python # You don't need to use this if you used "with client" await client.disconnect() """ if self.loop.is_running(): # Disconnect may be called from an event handler, which would # cancel itself during itself and never actually complete the # disconnection. Shield the task to prevent disconnect itself # from being cancelled. See issue #3942 for more details. return asyncio.shield(self.loop.create_task(self._disconnect_coro())) else: try: self.loop.run_until_complete(self._disconnect_coro()) except RuntimeError: # Python 3.5.x complains when called from # `__aexit__` and there were pending updates with: # "Event loop stopped before Future completed." # # However, it doesn't really make a lot of sense. pass def set_proxy(self: 'TelegramClient', proxy: typing.Union[tuple, dict]): """ Changes the proxy which will be used on next (re)connection. Method has no immediate effects if the client is currently connected. The new proxy will take it's effect on the next reconnection attempt: - on a call `await client.connect()` (after complete disconnect) - on auto-reconnect attempt (e.g, after previous connection was lost) """ init_proxy = None if not issubclass(self._connection, TcpMTProxy) else \ types.InputClientProxy(*self._connection.address_info(proxy)) self._init_request.proxy = init_proxy self._proxy = proxy # While `await client.connect()` passes new proxy on each new call, # auto-reconnect attempts use already set up `_connection` inside # the `_sender`, so the only way to change proxy between those # is to directly inject parameters. connection = getattr(self._sender, "_connection", None) if connection: if isinstance(connection, TcpMTProxy): connection._ip = proxy[0] connection._port = proxy[1] else: connection._proxy = proxy def _save_states_and_entities(self: 'TelegramClient'): entities = self._mb_entity_cache.get_all_entities() # Piggy-back on an arbitrary TL type with users and chats so the session can understand to read the entities. # It doesn't matter if we put users in the list of chats. self.session.process_entities(types.contacts.ResolvedPeer(None, [e._as_input_peer() for e in entities], [])) # As a hack to not need to change the session files, save ourselves with ``id=0`` and ``access_hash`` of our ``id``. # This way it is possible to determine our own ID by querying for 0. However, whether we're a bot is not saved. if self._mb_entity_cache.self_id: self.session.process_entities(types.contacts.ResolvedPeer(None, [types.InputPeerUser(0, self._mb_entity_cache.self_id)], [])) ss, cs = self._message_box.session_state() self.session.set_update_state(0, types.updates.State(**ss, unread_count=0)) now = datetime.datetime.now() # any datetime works; channels don't need it for channel_id, pts in cs.items(): self.session.set_update_state(channel_id, types.updates.State(pts, 0, now, 0, unread_count=0)) async def _disconnect_coro(self: 'TelegramClient'): if self.session is None: return # already logged out and disconnected await self._disconnect() # Also clean-up all exported senders because we're done with them async with self._borrow_sender_lock: for state, sender in self._borrowed_senders.values(): # Note that we're not checking for `state.should_disconnect()`. # If the user wants to disconnect the client, ALL connections # to Telegram (including exported senders) should be closed. # # Disconnect should never raise, so there's no try/except. await sender.disconnect() # Can't use `mark_disconnected` because it may be borrowed. state._connected = False # If any was borrowed self._borrowed_senders.clear() # trio's nurseries would handle this for us, but this is asyncio. # All tasks spawned in the background should properly be terminated. if self._event_handler_tasks: for task in self._event_handler_tasks: task.cancel() await asyncio.wait(self._event_handler_tasks) self._event_handler_tasks.clear() self._save_states_and_entities() self.session.close() async def _disconnect(self: 'TelegramClient'): """ Disconnect only, without closing the session. Used in reconnections to different data centers, where we don't want to close the session file; user disconnects however should close it since it means that their job with the client is complete and we should clean it up all. """ await self._sender.disconnect() await helpers._cancel(self._log[__name__], updates_handle=self._updates_handle, keepalive_handle=self._keepalive_handle) async def _switch_dc(self: 'TelegramClient', new_dc): """ Permanently switches the current connection to the new data center. """ self._log[__name__].info('Reconnecting to new data center %s', new_dc) dc = await self._get_dc(new_dc) self.session.set_dc(dc.id, dc.ip_address, dc.port) # auth_key's are associated with a server, which has now changed # so it's not valid anymore. Set to None to force recreating it. self._sender.auth_key.key = None self.session.auth_key = None self.session.save() await self._disconnect() return await self.connect() def _auth_key_callback(self: 'TelegramClient', auth_key): """ Callback from the sender whenever it needed to generate a new authorization key. This means we are not authorized. """ self.session.auth_key = auth_key self.session.save() # endregion # region Working with different connections/Data Centers async def _get_dc(self: 'TelegramClient', dc_id, cdn=False): """Gets the Data Center (DC) associated to 'dc_id'""" cls = self.__class__ if not cls._config: cls._config = await self(functions.help.GetConfigRequest()) if cdn and not self._cdn_config: cls._cdn_config = await self(functions.help.GetCdnConfigRequest()) for pk in cls._cdn_config.public_keys: if pk.dc_id == dc_id: rsa.add_key(pk.public_key, old=False) try: return next( dc for dc in cls._config.dc_options if dc.id == dc_id and bool(dc.ipv6) == self._use_ipv6 and bool(dc.cdn) == cdn ) except StopIteration: self._log[__name__].warning( 'Failed to get DC %s (cdn = %s) with use_ipv6 = %s; retrying ignoring IPv6 check', dc_id, cdn, self._use_ipv6 ) try: return next( dc for dc in cls._config.dc_options if dc.id == dc_id and bool(dc.cdn) == cdn ) except StopIteration: raise ValueError(f'Failed to get DC {dc_id} (cdn = {cdn})') async def _create_exported_sender(self: 'TelegramClient', dc_id): """ Creates a new exported `MTProtoSender` for the given `dc_id` and returns it. This method should be used by `_borrow_exported_sender`. """ # Thanks badoualy/kotlogram on /telegram/api/DefaultTelegramClient.kt # for clearly showing how to export the authorization dc = await self._get_dc(dc_id) # Can't reuse self._sender._connection as it has its own seqno. # # If one were to do that, Telegram would reset the connection # with no further clues. sender = MTProtoSender(None, loggers=self._log) await sender.connect(self._connection( dc.ip_address, dc.port, dc.id, loggers=self._log, proxy=self._proxy, local_addr=self._local_addr )) self._log[__name__].info('Exporting auth for new borrowed sender in %s', dc) auth = await self(functions.auth.ExportAuthorizationRequest(dc_id)) self._init_request.query = functions.auth.ImportAuthorizationRequest(id=auth.id, bytes=auth.bytes) req = functions.InvokeWithLayerRequest(LAYER, self._init_request) await sender.send(req) return sender async def _borrow_exported_sender(self: 'TelegramClient', dc_id): """ Borrows a connected `MTProtoSender` for the given `dc_id`. If it's not cached, creates a new one if it doesn't exist yet, and imports a freshly exported authorization key for it to be usable. Once its job is over it should be `_return_exported_sender`. """ async with self._borrow_sender_lock: self._log[__name__].debug('Borrowing sender for dc_id %d', dc_id) state, sender = self._borrowed_senders.get(dc_id, (None, None)) if state is None: state = _ExportState() sender = await self._create_exported_sender(dc_id) sender.dc_id = dc_id self._borrowed_senders[dc_id] = (state, sender) elif state.need_connect(): dc = await self._get_dc(dc_id) await sender.connect(self._connection( dc.ip_address, dc.port, dc.id, loggers=self._log, proxy=self._proxy, local_addr=self._local_addr )) state.add_borrow() return sender async def _return_exported_sender(self: 'TelegramClient', sender): """ Returns a borrowed exported sender. If all borrows have been returned, the sender is cleanly disconnected. """ async with self._borrow_sender_lock: self._log[__name__].debug('Returning borrowed sender for dc_id %d', sender.dc_id) state, _ = self._borrowed_senders[sender.dc_id] state.add_return() async def _clean_exported_senders(self: 'TelegramClient'): """ Cleans-up all unused exported senders by disconnecting them. """ async with self._borrow_sender_lock: for dc_id, (state, sender) in self._borrowed_senders.items(): if state.should_disconnect(): self._log[__name__].info( 'Disconnecting borrowed sender for DC %d', dc_id) # Disconnect should never raise await sender.disconnect() state.mark_disconnected() async def _get_cdn_client(self: 'TelegramClient', cdn_redirect): """Similar to ._borrow_exported_client, but for CDNs""" session = self._exported_sessions.get(cdn_redirect.dc_id) if not session: dc = await self._get_dc(cdn_redirect.dc_id, cdn=True) session = self.session.clone() session.set_dc(dc.id, dc.ip_address, dc.port) self._exported_sessions[cdn_redirect.dc_id] = session self._log[__name__].info('Creating new CDN client') client = self.__class__( session, self.api_id, self.api_hash, proxy=self._proxy, timeout=self._timeout, loop=self.loop ) session.auth_key = self._sender.auth_key await client._sender.connect(self._connection( session.server_address, session.port, session.dc_id, loggers=self._log, proxy=self._proxy, local_addr=self._local_addr )) return client # endregion # region Invoking Telegram requests @abc.abstractmethod def __call__(self: 'TelegramClient', request, ordered=False): """ Invokes (sends) one or more MTProtoRequests and returns (receives) their result. Args: request (`TLObject` | `list`): The request or requests to be invoked. ordered (`bool`, optional): Whether the requests (if more than one was given) should be executed sequentially on the server. They run in arbitrary order by default. flood_sleep_threshold (`int` | `None`, optional): The flood sleep threshold to use for this request. This overrides the default value stored in `client.flood_sleep_threshold ` Returns: The result of the request (often a `TLObject`) or a list of results if more than one request was given. """ raise NotImplementedError @abc.abstractmethod def _update_loop(self: 'TelegramClient'): raise NotImplementedError @abc.abstractmethod async def _handle_auto_reconnect(self: 'TelegramClient'): raise NotImplementedError # endregion Telethon-1.39.0/telethon/client/telegramclient.py000066400000000000000000000007361475566265000220650ustar00rootroot00000000000000from . import ( AccountMethods, AuthMethods, DownloadMethods, DialogMethods, ChatMethods, BotMethods, MessageMethods, UploadMethods, ButtonMethods, UpdateMethods, MessageParseMethods, UserMethods, TelegramBaseClient ) class TelegramClient( AccountMethods, AuthMethods, DownloadMethods, DialogMethods, ChatMethods, BotMethods, MessageMethods, UploadMethods, ButtonMethods, UpdateMethods, MessageParseMethods, UserMethods, TelegramBaseClient ): pass Telethon-1.39.0/telethon/client/updates.py000066400000000000000000000724341475566265000205370ustar00rootroot00000000000000import asyncio import inspect import itertools import random import sys import time import traceback import typing import logging import warnings from collections import deque import sqlite3 from .. import events, utils, errors from ..events.common import EventBuilder, EventCommon from ..tl import types, functions from .._updates import GapError, PrematureEndReason from ..helpers import get_running_loop from ..version import __version__ if typing.TYPE_CHECKING: from .telegramclient import TelegramClient Callback = typing.Callable[[typing.Any], typing.Any] class UpdateMethods: # region Public methods async def _run_until_disconnected(self: 'TelegramClient'): try: # Make a high-level request to notify that we want updates await self(functions.updates.GetStateRequest()) result = await self.disconnected if self._updates_error is not None: raise self._updates_error return result except KeyboardInterrupt: pass finally: await self.disconnect() async def set_receive_updates(self: 'TelegramClient', receive_updates): """ Change the value of `receive_updates`. This is an `async` method, because in order for Telegram to start sending updates again, a request must be made. """ self._no_updates = not receive_updates if receive_updates: await self(functions.updates.GetStateRequest()) def run_until_disconnected(self: 'TelegramClient'): """ Runs the event loop until the library is disconnected. It also notifies Telegram that we want to receive updates as described in https://core.telegram.org/api/updates. If an unexpected error occurs during update handling, the client will disconnect and said error will be raised. Manual disconnections can be made by calling `disconnect() ` or sending a ``KeyboardInterrupt`` (e.g. by pressing ``Ctrl+C`` on the console window running the script). If a disconnection error occurs (i.e. the library fails to reconnect automatically), said error will be raised through here, so you have a chance to ``except`` it on your own code. If the loop is already running, this method returns a coroutine that you should await on your own code. .. note:: If you want to handle ``KeyboardInterrupt`` in your code, simply run the event loop in your code too in any way, such as ``loop.run_forever()`` or ``await client.disconnected`` (e.g. ``loop.run_until_complete(client.disconnected)``). Example .. code-block:: python # Blocks the current task here until a disconnection occurs. # # You will still receive updates, since this prevents the # script from exiting. await client.run_until_disconnected() """ if self.loop.is_running(): return self._run_until_disconnected() try: return self.loop.run_until_complete(self._run_until_disconnected()) except KeyboardInterrupt: pass finally: # No loop.run_until_complete; it's already syncified self.disconnect() def on(self: 'TelegramClient', event: EventBuilder): """ Decorator used to `add_event_handler` more conveniently. Arguments event (`_EventBuilder` | `type`): The event builder class or instance to be used, for instance ``events.NewMessage``. Example .. code-block:: python from telethon import TelegramClient, events client = TelegramClient(...) # Here we use client.on @client.on(events.NewMessage) async def handler(event): ... """ def decorator(f): self.add_event_handler(f, event) return f return decorator def add_event_handler( self: 'TelegramClient', callback: Callback, event: EventBuilder = None): """ Registers a new event handler callback. The callback will be called when the specified event occurs. Arguments callback (`callable`): The callable function accepting one parameter to be used. Note that if you have used `telethon.events.register` in the callback, ``event`` will be ignored, and instead the events you previously registered will be used. event (`_EventBuilder` | `type`, optional): The event builder class or instance to be used, for instance ``events.NewMessage``. If left unspecified, `telethon.events.raw.Raw` (the :tl:`Update` objects with no further processing) will be passed instead. Example .. code-block:: python from telethon import TelegramClient, events client = TelegramClient(...) async def handler(event): ... client.add_event_handler(handler, events.NewMessage) """ builders = events._get_handlers(callback) if builders is not None: for event in builders: self._event_builders.append((event, callback)) return if isinstance(event, type): event = event() elif not event: event = events.Raw() self._event_builders.append((event, callback)) def remove_event_handler( self: 'TelegramClient', callback: Callback, event: EventBuilder = None) -> int: """ Inverse operation of `add_event_handler()`. If no event is given, all events for this callback are removed. Returns how many callbacks were removed. Example .. code-block:: python @client.on(events.Raw) @client.on(events.NewMessage) async def handler(event): ... # Removes only the "Raw" handling # "handler" will still receive "events.NewMessage" client.remove_event_handler(handler, events.Raw) # "handler" will stop receiving anything client.remove_event_handler(handler) """ found = 0 if event and not isinstance(event, type): event = type(event) i = len(self._event_builders) while i: i -= 1 ev, cb = self._event_builders[i] if cb == callback and (not event or isinstance(ev, event)): del self._event_builders[i] found += 1 return found def list_event_handlers(self: 'TelegramClient')\ -> 'typing.Sequence[typing.Tuple[Callback, EventBuilder]]': """ Lists all registered event handlers. Returns A list of pairs consisting of ``(callback, event)``. Example .. code-block:: python @client.on(events.NewMessage(pattern='hello')) async def on_greeting(event): '''Greets someone''' await event.reply('Hi') for callback, event in client.list_event_handlers(): print(id(callback), type(event)) """ return [(callback, event) for event, callback in self._event_builders] async def catch_up(self: 'TelegramClient'): """ "Catches up" on the missed updates while the client was offline. You should call this method after registering the event handlers so that the updates it loads can by processed by your script. This can also be used to forcibly fetch new updates if there are any. Example .. code-block:: python await client.catch_up() """ await self._updates_queue.put(types.UpdatesTooLong()) # endregion # region Private methods async def _update_loop(self: 'TelegramClient'): # If the MessageBox is not empty, the account had to be logged-in to fill in its state. # This flag is used to propagate the "you got logged-out" error up (but getting logged-out # can only happen if it was once logged-in). was_once_logged_in = self._authorized is True or not self._message_box.is_empty() self._updates_error = None try: if self._catch_up: # User wants to catch up as soon as the client is up and running, # so this is the best place to do it. await self.catch_up() updates_to_dispatch = deque() while self.is_connected(): if updates_to_dispatch: if self._sequential_updates: await self._dispatch_update(updates_to_dispatch.popleft()) else: while updates_to_dispatch: # TODO if _dispatch_update fails for whatever reason, it's not logged! this should be fixed task = self.loop.create_task(self._dispatch_update(updates_to_dispatch.popleft())) self._event_handler_tasks.add(task) task.add_done_callback(self._event_handler_tasks.discard) continue if len(self._mb_entity_cache) >= self._entity_cache_limit: self._log[__name__].info( 'In-memory entity cache limit reached (%s/%s), flushing to session', len(self._mb_entity_cache), self._entity_cache_limit ) self._save_states_and_entities() self._mb_entity_cache.retain(lambda id: id == self._mb_entity_cache.self_id or id in self._message_box.map) if len(self._mb_entity_cache) >= self._entity_cache_limit: warnings.warn('in-memory entities exceed entity_cache_limit after flushing; consider setting a larger limit') self._log[__name__].info( 'In-memory entity cache at %s/%s after flushing to session', len(self._mb_entity_cache), self._entity_cache_limit ) get_diff = self._message_box.get_difference() if get_diff: self._log[__name__].debug('Getting difference for account updates') try: diff = await self(get_diff) except ( errors.ServerError, errors.TimedOutError, errors.FloodWaitError, ValueError ) as e: # Telegram is having issues self._log[__name__].info('Cannot get difference since Telegram is having issues: %s', type(e).__name__) self._message_box.end_difference() continue except (errors.UnauthorizedError, errors.AuthKeyError) as e: # Not logged in or broken authorization key, can't get difference self._log[__name__].info('Cannot get difference since the account is not logged in: %s', type(e).__name__) self._message_box.end_difference() if was_once_logged_in: self._updates_error = e await self.disconnect() break continue except (errors.TypeNotFoundError, sqlite3.OperationalError) as e: # User is likely doing weird things with their account or session and Telegram gets confused as to what layer they use self._log[__name__].warning('Cannot get difference since the account is likely misusing the session: %s', e) self._message_box.end_difference() self._updates_error = e await self.disconnect() break except OSError as e: # Network is likely down, but it's unclear for how long. # If disconnect is called this task will be cancelled along with the sleep. # If disconnect is not called, getting difference should be retried after a few seconds. self._log[__name__].info('Cannot get difference since the network is down: %s: %s', type(e).__name__, e) await asyncio.sleep(5) continue updates, users, chats = self._message_box.apply_difference(diff, self._mb_entity_cache) if updates: self._log[__name__].info('Got difference for account updates') updates_to_dispatch.extend(self._preprocess_updates(updates, users, chats)) continue get_diff = self._message_box.get_channel_difference(self._mb_entity_cache) if get_diff: self._log[__name__].debug('Getting difference for channel %s updates', get_diff.channel.channel_id) try: diff = await self(get_diff) except (errors.UnauthorizedError, errors.AuthKeyError) as e: # Not logged in or broken authorization key, can't get difference self._log[__name__].warning( 'Cannot get difference for channel %s since the account is not logged in: %s', get_diff.channel.channel_id, type(e).__name__ ) self._message_box.end_channel_difference( get_diff, PrematureEndReason.TEMPORARY_SERVER_ISSUES, self._mb_entity_cache ) if was_once_logged_in: self._updates_error = e await self.disconnect() break continue except (errors.TypeNotFoundError, sqlite3.OperationalError) as e: self._log[__name__].warning( 'Cannot get difference for channel %s since the account is likely misusing the session: %s', get_diff.channel.channel_id, e ) self._message_box.end_channel_difference( get_diff, PrematureEndReason.TEMPORARY_SERVER_ISSUES, self._mb_entity_cache ) self._updates_error = e await self.disconnect() break except ( errors.PersistentTimestampOutdatedError, errors.PersistentTimestampInvalidError, errors.ServerError, errors.TimedOutError, errors.FloodWaitError, ValueError ) as e: # According to Telegram's docs: # "Channel internal replication issues, try again later (treat this like an RPC_CALL_FAIL)." # We can treat this as "empty difference" and not update the local pts. # Then this same call will be retried when another gap is detected or timeout expires. # # Another option would be to literally treat this like an RPC_CALL_FAIL and retry after a few # seconds, but if Telegram is having issues it's probably best to wait for it to send another # update (hinting it may be okay now) and retry then. # # This is a bit hacky because MessageBox doesn't really have a way to "not update" the pts. # Instead we manually extract the previously-known pts and use that. # # For PersistentTimestampInvalidError: # Somehow our pts is either too new or the server does not know about this. # We treat this as PersistentTimestampOutdatedError for now. # TODO investigate why/when this happens and if this is the proper solution self._log[__name__].warning( 'Getting difference for channel updates %s caused %s;' ' ending getting difference prematurely until server issues are resolved', get_diff.channel.channel_id, type(e).__name__ ) self._message_box.end_channel_difference( get_diff, PrematureEndReason.TEMPORARY_SERVER_ISSUES, self._mb_entity_cache ) continue except (errors.ChannelPrivateError, errors.ChannelInvalidError): # Timeout triggered a get difference, but we have been banned in the channel since then. # Because we can no longer fetch updates from this channel, we should stop keeping track # of it entirely. self._log[__name__].info( 'Account is now banned in %d so we can no longer fetch updates from it', get_diff.channel.channel_id ) self._message_box.end_channel_difference( get_diff, PrematureEndReason.BANNED, self._mb_entity_cache ) continue except OSError as e: self._log[__name__].info( 'Cannot get difference for channel %d since the network is down: %s: %s', get_diff.channel.channel_id, type(e).__name__, e ) await asyncio.sleep(5) continue updates, users, chats = self._message_box.apply_channel_difference(get_diff, diff, self._mb_entity_cache) if updates: self._log[__name__].info('Got difference for channel %d updates', get_diff.channel.channel_id) updates_to_dispatch.extend(self._preprocess_updates(updates, users, chats)) continue deadline = self._message_box.check_deadlines() deadline_delay = deadline - get_running_loop().time() if deadline_delay > 0: # Don't bother sleeping and timing out if the delay is already 0 (pollutes the logs). try: updates = await asyncio.wait_for(self._updates_queue.get(), deadline_delay) except asyncio.TimeoutError: self._log[__name__].debug('Timeout waiting for updates expired') continue else: continue processed = [] try: users, chats = self._message_box.process_updates(updates, self._mb_entity_cache, processed) except GapError: continue # get(_channel)_difference will start returning requests updates_to_dispatch.extend(self._preprocess_updates(processed, users, chats)) except asyncio.CancelledError: pass except Exception as e: self._log[__name__].exception(f'Fatal error handling updates (this is a bug in Telethon v{__version__}, please report it)') self._updates_error = e await self.disconnect() def _preprocess_updates(self, updates, users, chats): self._mb_entity_cache.extend(users, chats) entities = {utils.get_peer_id(x): x for x in itertools.chain(users, chats)} for u in updates: u._entities = entities return updates async def _keepalive_loop(self: 'TelegramClient'): # Pings' ID don't really need to be secure, just "random" rnd = lambda: random.randrange(-2**63, 2**63) while self.is_connected(): try: await asyncio.wait_for( self.disconnected, timeout=60 ) continue # We actually just want to act upon timeout except asyncio.TimeoutError: pass except asyncio.CancelledError: return except Exception: continue # Any disconnected exception should be ignored # Check if we have any exported senders to clean-up periodically await self._clean_exported_senders() # Don't bother sending pings until the low-level connection is # ready, otherwise a lot of pings will be batched to be sent upon # reconnect, when we really don't care about that. if not self._sender._transport_connected(): continue # We also don't really care about their result. # Just send them periodically. try: self._sender._keepalive_ping(rnd()) except (ConnectionError, asyncio.CancelledError): return # Entities and cached files are not saved when they are # inserted because this is a rather expensive operation # (default's sqlite3 takes ~0.1s to commit changes). Do # it every minute instead. No-op if there's nothing new. self._save_states_and_entities() self.session.save() async def _dispatch_update(self: 'TelegramClient', update): # TODO only used for AlbumHack, and MessageBox is not really designed for this others = None if not self._mb_entity_cache.self_id: # Some updates require our own ID, so we must make sure # that the event builder has offline access to it. Calling # `get_me()` will cache it under `self._mb_entity_cache`. # # It will return `None` if we haven't logged in yet which is # fine, we will just retry next time anyway. try: await self.get_me(input_peer=True) except OSError: pass # might not have connection built = EventBuilderDict(self, update, others) for conv_set in self._conversations.values(): for conv in conv_set: ev = built[events.NewMessage] if ev: conv._on_new_message(ev) ev = built[events.MessageEdited] if ev: conv._on_edit(ev) ev = built[events.MessageRead] if ev: conv._on_read(ev) if conv._custom: await conv._check_custom(built) for builder, callback in self._event_builders: event = built[type(builder)] if not event: continue if not builder.resolved: await builder.resolve(self) filter = builder.filter(event) if inspect.isawaitable(filter): filter = await filter if not filter: continue try: await callback(event) except errors.AlreadyInConversationError: name = getattr(callback, '__name__', repr(callback)) self._log[__name__].debug( 'Event handler "%s" already has an open conversation, ' 'ignoring new one', name) except events.StopPropagation: name = getattr(callback, '__name__', repr(callback)) self._log[__name__].debug( 'Event handler "%s" stopped chain of propagation ' 'for event %s.', name, type(event).__name__ ) break except Exception as e: if not isinstance(e, asyncio.CancelledError) or self.is_connected(): name = getattr(callback, '__name__', repr(callback)) self._log[__name__].exception('Unhandled exception on %s', name) async def _dispatch_event(self: 'TelegramClient', event): """ Dispatches a single, out-of-order event. Used by `AlbumHack`. """ # We're duplicating a most logic from `_dispatch_update`, but all in # the name of speed; we don't want to make it worse for all updates # just because albums may need it. for builder, callback in self._event_builders: if isinstance(builder, events.Raw): continue if not isinstance(event, builder.Event): continue if not builder.resolved: await builder.resolve(self) filter = builder.filter(event) if inspect.isawaitable(filter): filter = await filter if not filter: continue try: await callback(event) except errors.AlreadyInConversationError: name = getattr(callback, '__name__', repr(callback)) self._log[__name__].debug( 'Event handler "%s" already has an open conversation, ' 'ignoring new one', name) except events.StopPropagation: name = getattr(callback, '__name__', repr(callback)) self._log[__name__].debug( 'Event handler "%s" stopped chain of propagation ' 'for event %s.', name, type(event).__name__ ) break except Exception as e: if not isinstance(e, asyncio.CancelledError) or self.is_connected(): name = getattr(callback, '__name__', repr(callback)) self._log[__name__].exception('Unhandled exception on %s', name) async def _handle_auto_reconnect(self: 'TelegramClient'): # TODO Catch-up # For now we make a high-level request to let Telegram # know we are still interested in receiving more updates. try: await self.get_me() except Exception as e: self._log[__name__].warning('Error executing high-level request ' 'after reconnect: %s: %s', type(e), e) return try: self._log[__name__].info( 'Asking for the current state after reconnect...') # TODO consider: # If there aren't many updates while the client is disconnected # (I tried with up to 20), Telegram seems to send them without # asking for them (via updates.getDifference). # # On disconnection, the library should probably set a "need # difference" or "catching up" flag so that any new updates are # ignored, and then the library should call updates.getDifference # itself to fetch them. # # In any case (either there are too many updates and Telegram # didn't send them, or there isn't a lot and Telegram sent them # but we dropped them), we fetch the new difference to get all # missed updates. I feel like this would be the best solution. # If a disconnection occurs, the old known state will be # the latest one we were aware of, so we can catch up since # the most recent state we were aware of. await self.catch_up() self._log[__name__].info('Successfully fetched missed updates') except errors.RPCError as e: self._log[__name__].warning('Failed to get missed updates after ' 'reconnect: %r', e) except Exception: self._log[__name__].exception( 'Unhandled exception while getting update difference after reconnect') # endregion class EventBuilderDict: """ Helper "dictionary" to return events from types and cache them. """ def __init__(self, client: 'TelegramClient', update, others): self.client = client self.update = update self.others = others def __getitem__(self, builder): try: return self.__dict__[builder] except KeyError: event = self.__dict__[builder] = builder.build( self.update, self.others, self.client._self_id) if isinstance(event, EventCommon): event.original_update = self.update event._entities = self.update._entities event._set_client(self.client) elif event: event._client = self.client return event Telethon-1.39.0/telethon/client/uploads.py000066400000000000000000001075721475566265000205430ustar00rootroot00000000000000import hashlib import io import itertools import os import pathlib import re import typing from io import BytesIO from ..crypto import AES from .. import utils, helpers, hints from ..tl import types, functions, custom try: import PIL import PIL.Image except ImportError: PIL = None if typing.TYPE_CHECKING: from .telegramclient import TelegramClient class _CacheType: """Like functools.partial but pretends to be the wrapped class.""" def __init__(self, cls): self._cls = cls def __call__(self, *args, **kwargs): return self._cls(*args, file_reference=b'', **kwargs) def __eq__(self, other): return self._cls == other def _resize_photo_if_needed( file, is_image, width=2560, height=2560, background=(255, 255, 255)): # https://github.com/telegramdesktop/tdesktop/blob/12905f0dcb9d513378e7db11989455a1b764ef75/Telegram/SourceFiles/boxes/photo_crop_box.cpp#L254 if (not is_image or PIL is None or (isinstance(file, io.IOBase) and not file.seekable())): return file if isinstance(file, bytes): file = io.BytesIO(file) if isinstance(file, io.IOBase): # Pillow seeks to 0 unconditionally later anyway old_pos = file.tell() file.seek(0, io.SEEK_END) before = file.tell() elif isinstance(file, str) and os.path.exists(file): # Check if file exists as a path and if so, get its size on disk before = os.path.getsize(file) else: # Would be weird... before = None try: # Don't use a `with` block for `image`, or `file` would be closed. # See https://github.com/LonamiWebs/Telethon/issues/1121 for more. image = PIL.Image.open(file) try: kwargs = {'exif': image.info['exif']} except KeyError: kwargs = {} if image.mode == 'RGB': # Check if image is within acceptable bounds, if so, check if the image is at or below 10 MB, or assume it isn't if size is None or 0 if image.width <= width and image.height <= height and (before <= 10000000 if before else False): return file # If the image is already RGB, don't convert it # certain modes such as 'P' have no alpha index but can't be saved as JPEG directly image.thumbnail((width, height), PIL.Image.LANCZOS) result = image else: # We could save the resized image with the original format, but # JPEG often compresses better -> smaller size -> faster upload # We need to mask away the alpha channel ([3]), since otherwise # IOError is raised when trying to save alpha channels in JPEG. image.thumbnail((width, height), PIL.Image.LANCZOS) result = PIL.Image.new('RGB', image.size, background) mask = None if image.has_transparency_data: if image.mode == 'RGBA': mask = image.getchannel('A') else: mask = image.convert('RGBA').getchannel('A') result.paste(image, mask=mask) buffer = io.BytesIO() result.save(buffer, 'JPEG', progressive=True, **kwargs) buffer.seek(0) buffer.name = 'a.jpg' return buffer except IOError: return file finally: # The original position might matter if isinstance(file, io.IOBase): file.seek(old_pos) class UploadMethods: # region Public methods async def send_file( self: 'TelegramClient', entity: 'hints.EntityLike', file: 'typing.Union[hints.FileLike, typing.Sequence[hints.FileLike]]', *, caption: typing.Union[str, typing.Sequence[str]] = None, force_document: bool = False, file_size: int = None, clear_draft: bool = False, progress_callback: 'hints.ProgressCallback' = None, reply_to: 'hints.MessageIDLike' = None, attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None, thumb: 'hints.FileLike' = None, allow_cache: bool = True, parse_mode: str = (), formatting_entities: typing.Optional[ typing.Union[ typing.List[types.TypeMessageEntity], typing.List[typing.List[types.TypeMessageEntity]] ] ] = None, voice_note: bool = False, video_note: bool = False, buttons: typing.Optional['hints.MarkupLike'] = None, silent: bool = None, background: bool = None, supports_streaming: bool = False, schedule: 'hints.DateLike' = None, comment_to: 'typing.Union[int, types.Message]' = None, ttl: int = None, nosound_video: bool = None, **kwargs) -> typing.Union[typing.List[typing.Any], typing.Any]: """ Sends message with the given file to the specified entity. .. note:: If the ``hachoir3`` package (``hachoir`` module) is installed, it will be used to determine metadata from audio and video files. If the ``pillow`` package is installed and you are sending a photo, it will be resized to fit within the maximum dimensions allowed by Telegram to avoid ``errors.PhotoInvalidDimensionsError``. This cannot be done if you are sending :tl:`InputFile`, however. Arguments entity (`entity`): Who will receive the file. file (`str` | `bytes` | `file` | `media`): The file to send, which can be one of: * A local file path to an in-disk file. The file name will be the path's base name. * A `bytes` byte array with the file's data to send (for example, by using ``text.encode('utf-8')``). A default file name will be used. * A bytes `io.IOBase` stream over the file to send (for example, by using ``open(file, 'rb')``). Its ``.name`` property will be used for the file name, or a default if it doesn't have one. * An external URL to a file over the internet. This will send the file as "external" media, and Telegram is the one that will fetch the media and send it. * A Bot API-like ``file_id``. You can convert previously sent media to file IDs for later reusing with `telethon.utils.pack_bot_file_id`. * A handle to an existing file (for example, if you sent a message with media before, you can use its ``message.media`` as a file here). * A handle to an uploaded file (from `upload_file`). * A :tl:`InputMedia` instance. For example, if you want to send a dice use :tl:`InputMediaDice`, or if you want to send a contact use :tl:`InputMediaContact`. To send an album, you should provide a list in this parameter. If a list or similar is provided, the files in it will be sent as an album in the order in which they appear, sliced in chunks of 10 if more than 10 are given. caption (`str`, optional): Optional caption for the sent media message. When sending an album, the caption may be a list of strings, which will be assigned to the files pairwise. force_document (`bool`, optional): If left to `False` and the file is a path that ends with the extension of an image file or a video file, it will be sent as such. Otherwise always as a document. file_size (`int`, optional): The size of the file to be uploaded if it needs to be uploaded, which will be determined automatically if not specified. If the file size can't be determined beforehand, the entire file will be read in-memory to find out how large it is. clear_draft (`bool`, optional): Whether the existing draft should be cleared or not. progress_callback (`callable`, optional): A callback function accepting two parameters: ``(sent bytes, total)``. reply_to (`int` | `Message `): Same as `reply_to` from `send_message`. attributes (`list`, optional): Optional attributes that override the inferred ones, like :tl:`DocumentAttributeFilename` and so on. thumb (`str` | `bytes` | `file`, optional): Optional JPEG thumbnail (for documents). **Telegram will ignore this parameter** unless you pass a ``.jpg`` file! The file must also be small in dimensions and in disk size. Successful thumbnails were files below 20kB and 320x320px. Width/height and dimensions/size ratios may be important. For Telegram to accept a thumbnail, you must provide the dimensions of the underlying media through ``attributes=`` with :tl:`DocumentAttributesVideo` or by installing the optional ``hachoir`` dependency. allow_cache (`bool`, optional): This parameter currently does nothing, but is kept for backward-compatibility (and it may get its use back in the future). parse_mode (`object`, optional): See the `TelegramClient.parse_mode ` property for allowed values. Markdown parsing will be used by default. formatting_entities (`list`, optional): Optional formatting entities for the sent media message. When sending an album, `formatting_entities` can be a list of lists, where each inner list contains `types.TypeMessageEntity`. Each inner list will be assigned to the corresponding file in a pairwise manner with the caption. If provided, the ``parse_mode`` parameter will be ignored. voice_note (`bool`, optional): If `True` the audio will be sent as a voice note. video_note (`bool`, optional): If `True` the video will be sent as a video note, also known as a round video message. buttons (`list`, `custom.Button `, :tl:`KeyboardButton`): The matrix (list of lists), row list or button to be shown after sending the message. This parameter will only work if you have signed in as a bot. You can also pass your own :tl:`ReplyMarkup` here. silent (`bool`, optional): Whether the message should notify people with sound or not. Defaults to `False` (send with a notification sound unless the person has the chat muted). Set it to `True` to alter this behaviour. background (`bool`, optional): Whether the message should be send in background. supports_streaming (`bool`, optional): Whether the sent video supports streaming or not. Note that Telegram only recognizes as streamable some formats like MP4, and others like AVI or MKV will not work. You should convert these to MP4 before sending if you want them to be streamable. Unsupported formats will result in ``VideoContentTypeError``. schedule (`hints.DateLike`, optional): If set, the file won't send immediately, and instead it will be scheduled to be automatically sent at a later time. comment_to (`int` | `Message `, optional): Similar to ``reply_to``, but replies in the linked group of a broadcast channel instead (effectively leaving a "comment to" the specified message). This parameter takes precedence over ``reply_to``. If there is no linked chat, `telethon.errors.sgIdInvalidError` is raised. ttl (`int`. optional): The Time-To-Live of the file (also known as "self-destruct timer" or "self-destructing media"). If set, files can only be viewed for a short period of time before they disappear from the message history automatically. The value must be at least 1 second, and at most 60 seconds, otherwise Telegram will ignore this parameter. Not all types of media can be used with this parameter, such as text documents, which will fail with ``TtlMediaInvalidError``. nosound_video (`bool`, optional): Only applicable when sending a video file without an audio track. If set to ``True``, the video will be displayed in Telegram as a video. If set to ``False``, Telegram will attempt to display the video as an animated gif. (It may still display as a video due to other factors.) The value is ignored if set on non-video files. This is set to ``True`` for albums, as gifs cannot be sent in albums. Returns The `Message ` (or messages) containing the sent file, or messages if a list of them was passed. Example .. code-block:: python # Normal files like photos await client.send_file(chat, '/my/photos/me.jpg', caption="It's me!") # or await client.send_message(chat, "It's me!", file='/my/photos/me.jpg') # Voice notes or round videos await client.send_file(chat, '/my/songs/song.mp3', voice_note=True) await client.send_file(chat, '/my/videos/video.mp4', video_note=True) # Custom thumbnails await client.send_file(chat, '/my/documents/doc.txt', thumb='photo.jpg') # Only documents await client.send_file(chat, '/my/photos/photo.png', force_document=True) # Albums await client.send_file(chat, [ '/my/photos/holiday1.jpg', '/my/photos/holiday2.jpg', '/my/drawings/portrait.png' ]) # Printing upload progress def callback(current, total): print('Uploaded', current, 'out of', total, 'bytes: {:.2%}'.format(current / total)) await client.send_file(chat, file, progress_callback=callback) # Dices, including dart and other future emoji from telethon.tl import types await client.send_file(chat, types.InputMediaDice('')) await client.send_file(chat, types.InputMediaDice('🎯')) # Contacts await client.send_file(chat, types.InputMediaContact( phone_number='+34 123 456 789', first_name='Example', last_name='', vcard='' )) """ # TODO Properly implement allow_cache to reuse the sha256 of the file # i.e. `None` was used if not file: raise TypeError('Cannot use {!r} as file'.format(file)) if not caption: caption = '' if not formatting_entities: formatting_entities = [] entity = await self.get_input_entity(entity) if comment_to is not None: entity, reply_to = await self._get_comment_data(entity, comment_to) else: reply_to = utils.get_message_id(reply_to) # First check if the user passed an iterable, in which case # we may want to send grouped. if utils.is_list_like(file): sent_count = 0 used_callback = None if not progress_callback else ( lambda s, t: progress_callback(sent_count + s, len(file)) ) if utils.is_list_like(caption): captions = caption else: captions = [caption] # Check that formatting_entities list is valid if all(utils.is_list_like(obj) for obj in formatting_entities): formatting_entities = formatting_entities elif utils.is_list_like(formatting_entities): formatting_entities = [formatting_entities] else: raise TypeError('The formatting_entities argument must be a list or a sequence of lists') # Check that all entities in all lists are of the correct type if not all(isinstance(ent, types.TypeMessageEntity) for sublist in formatting_entities for ent in sublist): raise TypeError('All entities must be instances of ') result = [] while file: result += await self._send_album( entity, file[:10], caption=captions[:10], formatting_entities=formatting_entities[:10], progress_callback=used_callback, reply_to=reply_to, parse_mode=parse_mode, silent=silent, schedule=schedule, supports_streaming=supports_streaming, clear_draft=clear_draft, force_document=force_document, background=background, ) file = file[10:] captions = captions[10:] formatting_entities = formatting_entities[10:] sent_count += 10 return result if formatting_entities: msg_entities = formatting_entities else: caption, msg_entities =\ await self._parse_message_text(caption, parse_mode) file_handle, media, image = await self._file_to_media( file, force_document=force_document, file_size=file_size, progress_callback=progress_callback, attributes=attributes, allow_cache=allow_cache, thumb=thumb, voice_note=voice_note, video_note=video_note, supports_streaming=supports_streaming, ttl=ttl, nosound_video=nosound_video, ) # e.g. invalid cast from :tl:`MessageMediaWebPage` if not media: raise TypeError('Cannot use {!r} as file'.format(file)) markup = self.build_reply_markup(buttons) reply_to = None if reply_to is None else types.InputReplyToMessage(reply_to) request = functions.messages.SendMediaRequest( entity, media, reply_to=reply_to, message=caption, entities=msg_entities, reply_markup=markup, silent=silent, schedule_date=schedule, clear_draft=clear_draft, background=background ) return self._get_response_message(request, await self(request), entity) async def _send_album(self: 'TelegramClient', entity, files, caption='', formatting_entities=None, progress_callback=None, reply_to=None, parse_mode=(), silent=None, schedule=None, supports_streaming=None, clear_draft=None, force_document=False, background=None, ttl=None): """Specialized version of .send_file for albums""" # We don't care if the user wants to avoid cache, we will use it # anyway. Why? The cached version will be exactly the same thing # we need to produce right now to send albums (uploadMedia), and # cache only makes a difference for documents where the user may # want the attributes used on them to change. # # In theory documents can be sent inside the albums, but they appear # as different messages (not inside the album), and the logic to set # the attributes/avoid cache is already written in .send_file(). entity = await self.get_input_entity(entity) if not utils.is_list_like(caption): caption = (caption,) if not all(isinstance(obj, list) for obj in formatting_entities): formatting_entities = (formatting_entities,) captions = [] # If the formatting_entities argument is provided, we don't use parse_mode if formatting_entities: # Pop from the end (so reverse) capt_with_ent = itertools.zip_longest(reversed(caption), reversed(formatting_entities), fillvalue=None) for msg_caption, msg_entities in capt_with_ent: captions.append((msg_caption, msg_entities)) else: for c in reversed(caption): # Pop from the end (so reverse) captions.append(await self._parse_message_text(c or '', parse_mode)) reply_to = utils.get_message_id(reply_to) used_callback = None if not progress_callback else ( # use an integer when sent matches total, to easily determine a file has been fully sent lambda s, t: progress_callback(sent_count + 1 if s == t else sent_count + s / t, len(files)) ) # Need to upload the media first, but only if they're not cached yet media = [] for sent_count, file in enumerate(files): # Albums want :tl:`InputMedia` which, in theory, includes # :tl:`InputMediaUploadedPhoto`. However, using that will # make it `raise MediaInvalidError`, so we need to upload # it as media and then convert that to :tl:`InputMediaPhoto`. fh, fm, _ = await self._file_to_media( file, supports_streaming=supports_streaming, force_document=force_document, ttl=ttl, progress_callback=used_callback, nosound_video=True) if isinstance(fm, (types.InputMediaUploadedPhoto, types.InputMediaPhotoExternal)): r = await self(functions.messages.UploadMediaRequest( entity, media=fm )) fm = utils.get_input_media(r.photo) elif isinstance(fm, (types.InputMediaUploadedDocument, types.InputMediaDocumentExternal)): r = await self(functions.messages.UploadMediaRequest( entity, media=fm )) fm = utils.get_input_media( r.document, supports_streaming=supports_streaming) if captions: caption, msg_entities = captions.pop() else: caption, msg_entities = '', None media.append(types.InputSingleMedia( fm, message=caption, entities=msg_entities # random_id is autogenerated )) # Now we can construct the multi-media request request = functions.messages.SendMultiMediaRequest( entity, reply_to=None if reply_to is None else types.InputReplyToMessage(reply_to), multi_media=media, silent=silent, schedule_date=schedule, clear_draft=clear_draft, background=background ) result = await self(request) random_ids = [m.random_id for m in media] return self._get_response_message(random_ids, result, entity) async def upload_file( self: 'TelegramClient', file: 'hints.FileLike', *, part_size_kb: float = None, file_size: int = None, file_name: str = None, use_cache: type = None, key: bytes = None, iv: bytes = None, progress_callback: 'hints.ProgressCallback' = None) -> 'types.TypeInputFile': """ Uploads a file to Telegram's servers, without sending it. .. note:: Generally, you want to use `send_file` instead. This method returns a handle (an instance of :tl:`InputFile` or :tl:`InputFileBig`, as required) which can be later used before it expires (they are usable during less than a day). Uploading a file will simply return a "handle" to the file stored remotely in the Telegram servers, which can be later used on. This will **not** upload the file to your own chat or any chat at all. Arguments file (`str` | `bytes` | `file`): The path of the file, byte array, or stream that will be sent. Note that if a byte array or a stream is given, a filename or its type won't be inferred, and it will be sent as an "unnamed application/octet-stream". part_size_kb (`int`, optional): Chunk size when uploading files. The larger, the less requests will be made (up to 512KB maximum). file_size (`int`, optional): The size of the file to be uploaded, which will be determined automatically if not specified. If the file size can't be determined beforehand, the entire file will be read in-memory to find out how large it is. file_name (`str`, optional): The file name which will be used on the resulting InputFile. If not specified, the name will be taken from the ``file`` and if this is not a `str`, it will be ``"unnamed"``. use_cache (`type`, optional): This parameter currently does nothing, but is kept for backward-compatibility (and it may get its use back in the future). key ('bytes', optional): In case of an encrypted upload (secret chats) a key is supplied iv ('bytes', optional): In case of an encrypted upload (secret chats) an iv is supplied progress_callback (`callable`, optional): A callback function accepting two parameters: ``(sent bytes, total)``. When sending an album, the callback will receive a number between 0 and the amount of files as the "sent" parameter, and the amount of files as the "total". Note that the first parameter will be a floating point number to indicate progress within a file (e.g. ``2.5`` means it has sent 50% of the third file, because it's between 2 and 3). Returns :tl:`InputFileBig` if the file size is larger than 10MB, `InputSizedFile ` (subclass of :tl:`InputFile`) otherwise. Example .. code-block:: python # Photos as photo and document file = await client.upload_file('photo.jpg') await client.send_file(chat, file) # sends as photo await client.send_file(chat, file, force_document=True) # sends as document file.name = 'not a photo.jpg' await client.send_file(chat, file, force_document=True) # document, new name # As song or as voice note file = await client.upload_file('song.ogg') await client.send_file(chat, file) # sends as song await client.send_file(chat, file, voice_note=True) # sends as voice note """ if isinstance(file, (types.InputFile, types.InputFileBig)): return file # Already uploaded pos = 0 async with helpers._FileStream(file, file_size=file_size) as stream: # Opening the stream will determine the correct file size file_size = stream.file_size if not part_size_kb: part_size_kb = utils.get_appropriated_part_size(file_size) if part_size_kb > 512: raise ValueError('The part size must be less or equal to 512KB') part_size = int(part_size_kb * 1024) if part_size % 1024 != 0: raise ValueError( 'The part size must be evenly divisible by 1024') # Set a default file name if None was specified file_id = helpers.generate_random_long() if not file_name: file_name = stream.name or str(file_id) # If the file name lacks extension, add it if possible. # Else Telegram complains with `PHOTO_EXT_INVALID_ERROR` # even if the uploaded image is indeed a photo. if not os.path.splitext(file_name)[-1]: file_name += utils._get_extension(stream) # Determine whether the file is too big (over 10MB) or not # Telegram does make a distinction between smaller or larger files is_big = file_size > 10 * 1024 * 1024 hash_md5 = hashlib.md5() part_count = (file_size + part_size - 1) // part_size self._log[__name__].info('Uploading file of %d bytes in %d chunks of %d', file_size, part_count, part_size) pos = 0 for part_index in range(part_count): # Read the file by in chunks of size part_size part = await helpers._maybe_await(stream.read(part_size)) if not isinstance(part, bytes): raise TypeError( 'file descriptor returned {}, not bytes (you must ' 'open the file in bytes mode)'.format(type(part))) # `file_size` could be wrong in which case `part` may not be # `part_size` before reaching the end. if len(part) != part_size and part_index < part_count - 1: raise ValueError( 'read less than {} before reaching the end; either ' '`file_size` or `read` are wrong'.format(part_size)) pos += len(part) # Encryption part if needed if key and iv: part = AES.encrypt_ige(part, key, iv) if not is_big: # Bit odd that MD5 is only needed for small files and not # big ones with more chance for corruption, but that's # what Telegram wants. hash_md5.update(part) # The SavePartRequest is different depending on whether # the file is too large or not (over or less than 10MB) if is_big: request = functions.upload.SaveBigFilePartRequest( file_id, part_index, part_count, part) else: request = functions.upload.SaveFilePartRequest( file_id, part_index, part) result = await self(request) if result: self._log[__name__].debug('Uploaded %d/%d', part_index + 1, part_count) if progress_callback: await helpers._maybe_await(progress_callback(pos, file_size)) else: raise RuntimeError( 'Failed to upload file part {}.'.format(part_index)) if is_big: return types.InputFileBig(file_id, part_count, file_name) else: return custom.InputSizedFile( file_id, part_count, file_name, md5=hash_md5, size=file_size ) # endregion async def _file_to_media( self, file, force_document=False, file_size=None, progress_callback=None, attributes=None, thumb=None, allow_cache=True, voice_note=False, video_note=False, supports_streaming=False, mime_type=None, as_image=None, ttl=None, nosound_video=None): if not file: return None, None, None if isinstance(file, pathlib.Path): file = str(file.absolute()) is_image = utils.is_image(file) if as_image is None: as_image = is_image and not force_document # `aiofiles` do not base `io.IOBase` but do have `read`, so we # just check for the read attribute to see if it's file-like. if not isinstance(file, (str, bytes, types.InputFile, types.InputFileBig)) \ and not hasattr(file, 'read'): # The user may pass a Message containing media (or the media, # or anything similar) that should be treated as a file. Try # getting the input media for whatever they passed and send it. # # We pass all attributes since these will be used if the user # passed :tl:`InputFile`, and all information may be relevant. try: return (None, utils.get_input_media( file, is_photo=as_image, attributes=attributes, force_document=force_document, voice_note=voice_note, video_note=video_note, supports_streaming=supports_streaming, ttl=ttl ), as_image) except TypeError: # Can't turn whatever was given into media return None, None, as_image media = None file_handle = None if isinstance(file, (types.InputFile, types.InputFileBig)): file_handle = file elif not isinstance(file, str) or os.path.isfile(file): file_handle = await self.upload_file( _resize_photo_if_needed(file, as_image), file_size=file_size, progress_callback=progress_callback ) elif re.match('https?://', file): if as_image: media = types.InputMediaPhotoExternal(file, ttl_seconds=ttl) else: media = types.InputMediaDocumentExternal(file, ttl_seconds=ttl) else: bot_file = utils.resolve_bot_file_id(file) if bot_file: media = utils.get_input_media(bot_file, ttl=ttl) if media: pass # Already have media, don't check the rest elif not file_handle: raise ValueError( 'Failed to convert {} to media. Not an existing file, ' 'an HTTP URL or a valid bot-API-like file ID'.format(file) ) elif as_image: media = types.InputMediaUploadedPhoto(file_handle, ttl_seconds=ttl) else: attributes, mime_type = utils.get_attributes( file, mime_type=mime_type, attributes=attributes, force_document=force_document and not is_image, voice_note=voice_note, video_note=video_note, supports_streaming=supports_streaming, thumb=thumb ) if not thumb: thumb = None else: if isinstance(thumb, pathlib.Path): thumb = str(thumb.absolute()) thumb = await self.upload_file(thumb, file_size=file_size) # setting `nosound_video` to `True` doesn't affect videos with sound # instead it prevents sending silent videos as GIFs nosound_video = nosound_video if mime_type.split("/")[0] == 'video' else None media = types.InputMediaUploadedDocument( file=file_handle, mime_type=mime_type, attributes=attributes, thumb=thumb, force_file=force_document and not is_image, ttl_seconds=ttl, nosound_video=nosound_video ) return file_handle, media, as_image # endregion Telethon-1.39.0/telethon/client/users.py000066400000000000000000000621241475566265000202260ustar00rootroot00000000000000import asyncio import datetime import itertools import time import typing from .. import errors, helpers, utils, hints from ..errors import MultiError, RPCError from ..helpers import retry_range from ..tl import TLRequest, types, functions _NOT_A_REQUEST = lambda: TypeError('You can only invoke requests, not types!') if typing.TYPE_CHECKING: from .telegramclient import TelegramClient def _fmt_flood(delay, request, *, early=False, td=datetime.timedelta): return ( 'Sleeping%s for %ds (%s) on %s flood wait', ' early' if early else '', delay, td(seconds=delay), request.__class__.__name__ ) class UserMethods: async def __call__(self: 'TelegramClient', request, ordered=False, flood_sleep_threshold=None): return await self._call(self._sender, request, ordered=ordered) async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sleep_threshold=None): if self._loop is not None and self._loop != helpers.get_running_loop(): raise RuntimeError('The asyncio event loop must not change after connection (see the FAQ for details)') # if the loop is None it will fail with a connection error later on if flood_sleep_threshold is None: flood_sleep_threshold = self.flood_sleep_threshold requests = list(request) if utils.is_list_like(request) else [request] request = list(request) if utils.is_list_like(request) else request for i, r in enumerate(requests): if not isinstance(r, TLRequest): raise _NOT_A_REQUEST() await r.resolve(self, utils) # Avoid making the request if it's already in a flood wait if r.CONSTRUCTOR_ID in self._flood_waited_requests: due = self._flood_waited_requests[r.CONSTRUCTOR_ID] diff = round(due - time.time()) if diff <= 3: # Flood waits below 3 seconds are "ignored" self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None) elif diff <= flood_sleep_threshold: self._log[__name__].info(*_fmt_flood(diff, r, early=True)) await asyncio.sleep(diff) self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None) else: raise errors.FloodWaitError(request=r, capture=diff) if self._no_updates: if utils.is_list_like(request): request[i] = functions.InvokeWithoutUpdatesRequest(r) else: # This should only run once as requests should be a list of 1 item request = functions.InvokeWithoutUpdatesRequest(r) request_index = 0 last_error = None self._last_request = time.time() for attempt in retry_range(self._request_retries): try: future = sender.send(request, ordered=ordered) if isinstance(future, list): results = [] exceptions = [] for f in future: try: result = await f except RPCError as e: exceptions.append(e) results.append(None) continue self.session.process_entities(result) exceptions.append(None) results.append(result) request_index += 1 if any(x is not None for x in exceptions): raise MultiError(exceptions, results, requests) else: return results else: result = await future self.session.process_entities(result) return result except (errors.ServerError, errors.RpcCallFailError, errors.RpcMcgetFailError, errors.InterdcCallErrorError, errors.TimedOutError, errors.InterdcCallRichErrorError) as e: last_error = e self._log[__name__].warning( 'Telegram is having internal issues %s: %s', e.__class__.__name__, e) await asyncio.sleep(2) except (errors.FloodWaitError, errors.FloodPremiumWaitError, errors.SlowModeWaitError, errors.FloodTestPhoneWaitError) as e: last_error = e if utils.is_list_like(request): request = request[request_index] # SLOW_MODE_WAIT is chat-specific, not request-specific if not isinstance(e, errors.SlowModeWaitError): self._flood_waited_requests\ [request.CONSTRUCTOR_ID] = time.time() + e.seconds # In test servers, FLOOD_WAIT_0 has been observed, and sleeping for # such a short amount will cause retries very fast leading to issues. if e.seconds == 0: e.seconds = 1 if e.seconds <= self.flood_sleep_threshold: self._log[__name__].info(*_fmt_flood(e.seconds, request)) await asyncio.sleep(e.seconds) else: raise except (errors.PhoneMigrateError, errors.NetworkMigrateError, errors.UserMigrateError) as e: last_error = e self._log[__name__].info('Phone migrated to %d', e.new_dc) should_raise = isinstance(e, ( errors.PhoneMigrateError, errors.NetworkMigrateError )) if should_raise and await self.is_user_authorized(): raise await self._switch_dc(e.new_dc) if self._raise_last_call_error and last_error is not None: raise last_error raise ValueError('Request was unsuccessful {} time(s)' .format(attempt)) # region Public methods async def get_me(self: 'TelegramClient', input_peer: bool = False) \ -> 'typing.Union[types.User, types.InputPeerUser]': """ Gets "me", the current :tl:`User` who is logged in. If the user has not logged in yet, this method returns `None`. Arguments input_peer (`bool`, optional): Whether to return the :tl:`InputPeerUser` version or the normal :tl:`User`. This can be useful if you just need to know the ID of yourself. Returns Your own :tl:`User`. Example .. code-block:: python me = await client.get_me() print(me.username) """ if input_peer and self._mb_entity_cache.self_id: return self._mb_entity_cache.get(self._mb_entity_cache.self_id)._as_input_peer() try: me = (await self( functions.users.GetUsersRequest([types.InputUserSelf()])))[0] if not self._mb_entity_cache.self_id: self._mb_entity_cache.set_self_user(me.id, me.bot, me.access_hash) return utils.get_input_peer(me, allow_self=False) if input_peer else me except errors.UnauthorizedError: return None @property def _self_id(self: 'TelegramClient') -> typing.Optional[int]: """ Returns the ID of the logged-in user, if known. This property is used in every update, and some like `updateLoginToken` occur prior to login, so it gracefully handles when no ID is known yet. """ return self._mb_entity_cache.self_id async def is_bot(self: 'TelegramClient') -> bool: """ Return `True` if the signed-in user is a bot, `False` otherwise. Example .. code-block:: python if await client.is_bot(): print('Beep') else: print('Hello') """ if self._mb_entity_cache.self_bot is None: await self.get_me(input_peer=True) return self._mb_entity_cache.self_bot async def is_user_authorized(self: 'TelegramClient') -> bool: """ Returns `True` if the user is authorized (logged in). Example .. code-block:: python if not await client.is_user_authorized(): await client.send_code_request(phone) code = input('enter code: ') await client.sign_in(phone, code) """ if self._authorized is None: try: # Any request that requires authorization will work await self(functions.updates.GetStateRequest()) self._authorized = True except errors.RPCError: self._authorized = False return self._authorized async def get_entity( self: 'TelegramClient', entity: 'hints.EntitiesLike') -> typing.Union['hints.Entity', typing.List['hints.Entity']]: """ Turns the given entity into a valid Telegram :tl:`User`, :tl:`Chat` or :tl:`Channel`. You can also pass a list or iterable of entities, and they will be efficiently fetched from the network. Arguments entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`): If a username is given, **the username will be resolved** making an API call every time. Resolving usernames is an expensive operation and will start hitting flood waits around 50 usernames in a short period of time. If you want to get the entity for a *cached* username, you should first `get_input_entity(username) ` which will use the cache), and then use `get_entity` with the result of the previous call. Similar limits apply to invite links, and you should use their ID instead. Using phone numbers (from people in your contact list), exact names, integer IDs or :tl:`Peer` rely on a `get_input_entity` first, which in turn needs the entity to be in cache, unless a :tl:`InputPeer` was passed. Unsupported types will raise ``TypeError``. If the entity can't be found, ``ValueError`` will be raised. Returns :tl:`User`, :tl:`Chat` or :tl:`Channel` corresponding to the input entity. A list will be returned if more than one was given. Example .. code-block:: python from telethon import utils me = await client.get_entity('me') print(utils.get_display_name(me)) chat = await client.get_input_entity('username') async for message in client.iter_messages(chat): ... # Note that you could have used the username directly, but it's # good to use get_input_entity if you will reuse it a lot. async for message in client.iter_messages('username'): ... # Note that for this to work the phone number must be in your contacts some_id = await client.get_peer_id('+34123456789') """ single = not utils.is_list_like(entity) if single: entity = (entity,) # Group input entities by string (resolve username), # input users (get users), input chat (get chats) and # input channels (get channels) to get the most entities # in the less amount of calls possible. inputs = [] for x in entity: if isinstance(x, str): inputs.append(x) else: inputs.append(await self.get_input_entity(x)) lists = { helpers._EntityType.USER: [], helpers._EntityType.CHAT: [], helpers._EntityType.CHANNEL: [], } for x in inputs: try: lists[helpers._entity_type(x)].append(x) except TypeError: pass users = lists[helpers._EntityType.USER] chats = lists[helpers._EntityType.CHAT] channels = lists[helpers._EntityType.CHANNEL] if users: # GetUsersRequest has a limit of 200 per call tmp = [] while users: curr, users = users[:200], users[200:] tmp.extend(await self(functions.users.GetUsersRequest(curr))) users = tmp if chats: # TODO Handle chats slice? chats = (await self( functions.messages.GetChatsRequest([x.chat_id for x in chats]))).chats if channels: channels = (await self( functions.channels.GetChannelsRequest(channels))).chats # Merge users, chats and channels into a single dictionary id_entity = { # `get_input_entity` might've guessed the type from a non-marked ID, # so the only way to match that with the input is by not using marks here. utils.get_peer_id(x, add_mark=False): x for x in itertools.chain(users, chats, channels) } # We could check saved usernames and put them into the users, # chats and channels list from before. While this would reduce # the amount of ResolveUsername calls, it would fail to catch # username changes. result = [] for x in inputs: if isinstance(x, str): result.append(await self._get_entity_from_string(x)) elif not isinstance(x, types.InputPeerSelf): result.append(id_entity[utils.get_peer_id(x, add_mark=False)]) else: result.append(next( u for u in id_entity.values() if isinstance(u, types.User) and u.is_self )) return result[0] if single else result async def get_input_entity( self: 'TelegramClient', peer: 'hints.EntityLike') -> 'types.TypeInputPeer': """ Turns the given entity into its input entity version. Most requests use this kind of :tl:`InputPeer`, so this is the most suitable call to make for those cases. **Generally you should let the library do its job** and don't worry about getting the input entity first, but if you're going to use an entity often, consider making the call: Arguments entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`): If a username or invite link is given, **the library will use the cache**. This means that it's possible to be using a username that *changed* or an old invite link (this only happens if an invite link for a small group chat is used after it was upgraded to a mega-group). If the username or ID from the invite link is not found in the cache, it will be fetched. The same rules apply to phone numbers (``'+34 123456789'``) from people in your contact list. If an exact name is given, it must be in the cache too. This is not reliable as different people can share the same name and which entity is returned is arbitrary, and should be used only for quick tests. If a positive integer ID is given, the entity will be searched in cached users, chats or channels, without making any call. If a negative integer ID is given, the entity will be searched exactly as either a chat (prefixed with ``-``) or as a channel (prefixed with ``-100``). If a :tl:`Peer` is given, it will be searched exactly in the cache as either a user, chat or channel. If the given object can be turned into an input entity directly, said operation will be done. Unsupported types will raise ``TypeError``. If the entity can't be found, ``ValueError`` will be raised. Returns :tl:`InputPeerUser`, :tl:`InputPeerChat` or :tl:`InputPeerChannel` or :tl:`InputPeerSelf` if the parameter is ``'me'`` or ``'self'``. If you need to get the ID of yourself, you should use `get_me` with ``input_peer=True``) instead. Example .. code-block:: python # If you're going to use "username" often in your code # (make a lot of calls), consider getting its input entity # once, and then using the "user" everywhere instead. user = await client.get_input_entity('username') # The same applies to IDs, chats or channels. chat = await client.get_input_entity(-123456789) """ # Short-circuit if the input parameter directly maps to an InputPeer try: return utils.get_input_peer(peer) except TypeError: pass # Next in priority is having a peer (or its ID) cached in-memory try: # 0x2d45687 == crc32(b'Peer') if isinstance(peer, int) or peer.SUBCLASS_OF_ID == 0x2d45687: return self._mb_entity_cache.get(utils.get_peer_id(peer, add_mark=False))._as_input_peer() except AttributeError: pass # Then come known strings that take precedence if peer in ('me', 'self'): return types.InputPeerSelf() # No InputPeer, cached peer, or known string. Fetch from disk cache try: return self.session.get_input_entity(peer) except ValueError: pass # Only network left to try if isinstance(peer, str): return utils.get_input_peer( await self._get_entity_from_string(peer)) # If we're a bot and the user has messaged us privately users.getUsers # will work with access_hash = 0. Similar for channels.getChannels. # If we're not a bot but the user is in our contacts, it seems to work # regardless. These are the only two special-cased requests. peer = utils.get_peer(peer) if isinstance(peer, types.PeerUser): users = await self(functions.users.GetUsersRequest([ types.InputUser(peer.user_id, access_hash=0)])) if users and not isinstance(users[0], types.UserEmpty): # If the user passed a valid ID they expect to work for # channels but would be valid for users, we get UserEmpty. # Avoid returning the invalid empty input peer for that. # # We *could* try to guess if it's a channel first, and if # it's not, work as a chat and try to validate it through # another request, but that becomes too much work. return utils.get_input_peer(users[0]) elif isinstance(peer, types.PeerChat): return types.InputPeerChat(peer.chat_id) elif isinstance(peer, types.PeerChannel): try: channels = await self(functions.channels.GetChannelsRequest([ types.InputChannel(peer.channel_id, access_hash=0)])) return utils.get_input_peer(channels.chats[0]) except errors.ChannelInvalidError: pass raise ValueError( 'Could not find the input entity for {} ({}). Please read https://' 'docs.telethon.dev/en/stable/concepts/entities.html to' ' find out more details.' .format(peer, type(peer).__name__) ) async def _get_peer(self: 'TelegramClient', peer: 'hints.EntityLike'): i, cls = utils.resolve_id(await self.get_peer_id(peer)) return cls(i) async def get_peer_id( self: 'TelegramClient', peer: 'hints.EntityLike', add_mark: bool = True) -> int: """ Gets the ID for the given entity. This method needs to be ``async`` because `peer` supports usernames, invite-links, phone numbers (from people in your contact list), etc. If ``add_mark is False``, then a positive ID will be returned instead. By default, bot-API style IDs (signed) are returned. Example .. code-block:: python print(await client.get_peer_id('me')) """ if isinstance(peer, int): return utils.get_peer_id(peer, add_mark=add_mark) try: if peer.SUBCLASS_OF_ID not in (0x2d45687, 0xc91c90b6): # 0x2d45687, 0xc91c90b6 == crc32(b'Peer') and b'InputPeer' peer = await self.get_input_entity(peer) except AttributeError: peer = await self.get_input_entity(peer) if isinstance(peer, types.InputPeerSelf): peer = await self.get_me(input_peer=True) return utils.get_peer_id(peer, add_mark=add_mark) # endregion # region Private methods async def _get_entity_from_string(self: 'TelegramClient', string): """ Gets a full entity from the given string, which may be a phone or a username, and processes all the found entities on the session. The string may also be a user link, or a channel/chat invite link. This method has the side effect of adding the found users to the session database, so it can be queried later without API calls, if this option is enabled on the session. Returns the found entity, or raises TypeError if not found. """ phone = utils.parse_phone(string) if phone: try: for user in (await self( functions.contacts.GetContactsRequest(0))).users: if user.phone == phone: return user except errors.BotMethodInvalidError: raise ValueError('Cannot get entity by phone number as a ' 'bot (try using integer IDs, not strings)') elif string.lower() in ('me', 'self'): return await self.get_me() else: username, is_join_chat = utils.parse_username(string) if is_join_chat: invite = await self( functions.messages.CheckChatInviteRequest(username)) if isinstance(invite, types.ChatInvite): raise ValueError( 'Cannot get entity from a channel (or group) ' 'that you are not part of. Join the group and retry' ) elif isinstance(invite, types.ChatInviteAlready): return invite.chat elif username: try: result = await self( functions.contacts.ResolveUsernameRequest(username)) except errors.UsernameNotOccupiedError as e: raise ValueError('No user has "{}" as username' .format(username)) from e try: pid = utils.get_peer_id(result.peer, add_mark=False) if isinstance(result.peer, types.PeerUser): return next(x for x in result.users if x.id == pid) else: return next(x for x in result.chats if x.id == pid) except StopIteration: pass try: # Nobody with this username, maybe it's an exact name/title return await self.get_entity( self.session.get_input_entity(string)) except ValueError: pass raise ValueError( 'Cannot find any entity corresponding to "{}"'.format(string) ) async def _get_input_dialog(self: 'TelegramClient', dialog): """ Returns a :tl:`InputDialogPeer`. This is a bit tricky because it may or not need access to the client to convert what's given into an input entity. """ try: if dialog.SUBCLASS_OF_ID == 0xa21c9795: # crc32(b'InputDialogPeer') dialog.peer = await self.get_input_entity(dialog.peer) return dialog elif dialog.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer') return types.InputDialogPeer(dialog) except AttributeError: pass return types.InputDialogPeer(await self.get_input_entity(dialog)) async def _get_input_notify(self: 'TelegramClient', notify): """ Returns a :tl:`InputNotifyPeer`. This is a bit tricky because it may or not need access to the client to convert what's given into an input entity. """ try: if notify.SUBCLASS_OF_ID == 0x58981615: if isinstance(notify, types.InputNotifyPeer): notify.peer = await self.get_input_entity(notify.peer) return notify except AttributeError: pass return types.InputNotifyPeer(await self.get_input_entity(notify)) # endregion Telethon-1.39.0/telethon/crypto/000077500000000000000000000000001475566265000165505ustar00rootroot00000000000000Telethon-1.39.0/telethon/crypto/__init__.py000066400000000000000000000005351475566265000206640ustar00rootroot00000000000000""" 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 .aesctr import AESModeCTR from .authkey import AuthKey from .factorization import Factorization from .cdndecrypter import CdnDecrypter Telethon-1.39.0/telethon/crypto/aes.py000066400000000000000000000061021475566265000176710ustar00rootroot00000000000000""" AES IGE implementation in Python. If available, cryptg will be used instead, otherwise if available, libssl will be used instead, otherwise the Python implementation will be used. """ import os import pyaes import logging from . import libssl __log__ = logging.getLogger(__name__) try: import cryptg __log__.info('cryptg detected, it will be used for encryption') except ImportError: cryptg = None if libssl.encrypt_ige and libssl.decrypt_ige: __log__.info('libssl detected, it will be used for encryption') else: __log__.info('cryptg module not installed and libssl not found, ' 'falling back to (slower) Python encryption') 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. """ if cryptg: return cryptg.decrypt_ige(cipher_text, key, iv) if libssl.decrypt_ige: return libssl.decrypt_ige(cipher_text, key, iv) iv1 = iv[:len(iv) // 2] iv2 = iv[len(iv) // 2:] aes = pyaes.AES(key) plain_text = [] blocks_count = len(cipher_text) // 16 cipher_text_block = [0] * 16 for block_index in range(blocks_count): for i in range(16): cipher_text_block[i] = \ cipher_text[block_index * 16 + i] ^ iv2[i] plain_text_block = aes.decrypt(cipher_text_block) for i in range(16): plain_text_block[i] ^= iv1[i] iv1 = cipher_text[block_index * 16:block_index * 16 + 16] iv2 = plain_text_block plain_text.extend(plain_text_block) return bytes(plain_text) @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. """ padding = len(plain_text) % 16 if padding: plain_text += os.urandom(16 - padding) if cryptg: return cryptg.encrypt_ige(plain_text, key, iv) if libssl.encrypt_ige: return libssl.encrypt_ige(plain_text, key, iv) iv1 = iv[:len(iv) // 2] iv2 = iv[len(iv) // 2:] aes = pyaes.AES(key) cipher_text = [] blocks_count = len(plain_text) // 16 for block_index in range(blocks_count): plain_text_block = list( plain_text[block_index * 16:block_index * 16 + 16] ) for i in range(16): plain_text_block[i] ^= iv1[i] cipher_text_block = aes.encrypt(plain_text_block) for i in range(16): cipher_text_block[i] ^= iv2[i] iv1 = cipher_text_block iv2 = plain_text[block_index * 16:block_index * 16 + 16] cipher_text.extend(cipher_text_block) return bytes(cipher_text) Telethon-1.39.0/telethon/crypto/aesctr.py000066400000000000000000000023001475566265000203760ustar00rootroot00000000000000""" This module holds the AESModeCTR wrapper class. """ import pyaes class AESModeCTR: """Wrapper around pyaes.AESModeOfOperationCTR mode with custom IV""" # 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) assert isinstance(iv, bytes) assert len(iv) == 16 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) Telethon-1.39.0/telethon/crypto/authkey.py000066400000000000000000000035371475566265000206040ustar00rootroot00000000000000""" This module holds the AuthKey class. """ import struct from hashlib import sha1 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 @property def key(self): return self._key @key.setter def key(self, value): if not value: self._key = self.aux_hash = self.key_id = None return if isinstance(value, type(self)): self._key, self.aux_hash, self.key_id = \ value._key, value.aux_hash, value.key_id return self._key = value with BinaryReader(sha1(self._key).digest()) as reader: self.aux_hash = reader.read_long(signed=False) reader.read(4) self.key_id = reader.read_long(signed=False) # TODO This doesn't really fit here, it's only used in authentication def calc_new_nonce_hash(self, new_nonce, number): """ 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(' 1: break p, q = g, pq // g return (p, q) if p < q else (q, p) @staticmethod def gcd(a, b): """ Calculates the Greatest Common Divisor. :param a: the first number. :param b: the second number. :return: GCD(a, b) """ while b: a, b = b, a % b return a Telethon-1.39.0/telethon/crypto/libssl.py000066400000000000000000000106601475566265000204150ustar00rootroot00000000000000""" Helper module around the system's libssl library if available for IGE mode. """ import ctypes import ctypes.util import platform import sys try: import ctypes.macholib.dyld except ImportError: pass import logging import os __log__ = logging.getLogger(__name__) def _find_ssl_lib(): lib = ctypes.util.find_library('ssl') # macOS 10.15 segfaults on unversioned crypto libraries. # We therefore pin the current stable version here # Credit for fix goes to Sarah Harvey (@worldwise001) # https://www.shh.sh/2020/01/04/python-abort-trap-6.html if sys.platform == 'darwin': release, _version_info, _machine = platform.mac_ver() ver, major, *_ = release.split('.') # macOS 10.14 "mojave" is the last known major release # to support unversioned libssl.dylib. Anything above # needs specific versions if int(ver) > 10 or int(ver) == 10 and int(major) > 14: lib = ( ctypes.util.find_library('libssl.46') or ctypes.util.find_library('libssl.44') or ctypes.util.find_library('libssl.42') ) if not lib: raise OSError('no library called "ssl" found') # First, let ctypes try to handle it itself. try: libssl = ctypes.cdll.LoadLibrary(lib) except OSError: pass else: return libssl # This is a best-effort attempt at finding the full real path of lib. # # Unfortunately ctypes doesn't tell us *where* it finds the library, # so we have to do that ourselves. try: # This is not documented, so it could fail. Be on the safe side. paths = ctypes.macholib.dyld.DEFAULT_LIBRARY_FALLBACK except AttributeError: paths = [ os.path.expanduser("~/lib"), "/usr/local/lib", "/lib", "/usr/lib", ] for path in paths: if os.path.isdir(path): for root, _, files in os.walk(path): if lib in files: # Manually follow symbolic links on *nix systems. # Fix for https://github.com/LonamiWebs/Telethon/issues/1167 lib = os.path.realpath(os.path.join(root, lib)) return ctypes.cdll.LoadLibrary(lib) else: raise OSError('no absolute path for "%s" and cannot load by name' % lib) try: _libssl = _find_ssl_lib() except OSError as e: # See https://github.com/LonamiWebs/Telethon/issues/1167 # Sometimes `find_library` returns improper filenames. __log__.info('Failed to load SSL library: %s (%s)', type(e), e) _libssl = None if not _libssl: decrypt_ige = None encrypt_ige = None else: # https://github.com/openssl/openssl/blob/master/include/openssl/aes.h AES_ENCRYPT = ctypes.c_int(1) AES_DECRYPT = ctypes.c_int(0) AES_MAXNR = 14 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), ] def decrypt_ige(cipher_text, key, iv): aes_key = AES_KEY() key_len = ctypes.c_int(8 * len(key)) key = (ctypes.c_ubyte * len(key))(*key) iv = (ctypes.c_ubyte * len(iv))(*iv) in_len = ctypes.c_size_t(len(cipher_text)) in_ptr = (ctypes.c_ubyte * len(cipher_text))(*cipher_text) out_ptr = (ctypes.c_ubyte * len(cipher_text))() _libssl.AES_set_decrypt_key(key, key_len, ctypes.byref(aes_key)) _libssl.AES_ige_encrypt( ctypes.byref(in_ptr), ctypes.byref(out_ptr), in_len, ctypes.byref(aes_key), ctypes.byref(iv), AES_DECRYPT ) return bytes(out_ptr) def encrypt_ige(plain_text, key, iv): aes_key = AES_KEY() key_len = ctypes.c_int(8 * len(key)) key = (ctypes.c_ubyte * len(key))(*key) iv = (ctypes.c_ubyte * len(iv))(*iv) in_len = ctypes.c_size_t(len(plain_text)) in_ptr = (ctypes.c_ubyte * len(plain_text))(*plain_text) out_ptr = (ctypes.c_ubyte * len(plain_text))() _libssl.AES_set_encrypt_key(key, key_len, ctypes.byref(aes_key)) _libssl.AES_ige_encrypt( ctypes.byref(in_ptr), ctypes.byref(out_ptr), in_len, ctypes.byref(aes_key), ctypes.byref(iv), AES_ENCRYPT ) return bytes(out_ptr) Telethon-1.39.0/telethon/crypto/rsa.py000066400000000000000000000145751475566265000177230ustar00rootroot00000000000000""" This module holds several utilities regarding RSA and server fingerprints. """ import os import struct from hashlib import sha1 try: import rsa import rsa.core except ImportError: rsa = None raise ImportError('Missing module "rsa", please install via pip.') from ..tl import TLObject # {fingerprint: (Crypto.PublicKey.RSA._RSAobj, old)} dictionary _server_keys = {} def get_byte_array(integer): """Return the variable length bytes corresponding to the given int""" # Operate in big endian (unlike most of Telegram API) since: # > "...pq is a representation of a natural number # (in binary *big endian* format)..." # > "...current value of dh_prime equals # (in *big-endian* byte order)..." # Reference: https://core.telegram.org/mtproto/auth_key return int.to_bytes( integer, (integer.bit_length() + 8 - 1) // 8, # 8 bits per byte, byteorder='big', signed=False ) def _compute_fingerprint(key): """ 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)) # Telegram uses the last 8 bytes as the fingerprint return struct.unpack('>> from telethon import TelegramClient, events >>> client = TelegramClient(...) >>> >>> @client.on(events.NewMessage) ... async def delete(event): ... await event.delete() ... # No other event handler will have a chance to handle this event ... raise StopPropagation ... >>> @client.on(events.NewMessage) ... async def _(event): ... # Will never be reached, because it is the second handler ... pass """ # For some reason Sphinx wants the silly >>> or # it will show warnings and look bad when generated. pass def register(event=None): """ Decorator method to *register* event handlers. This is the client-less `add_event_handler() ` variant. Note that this method only registers callbacks as handlers, and does not attach them to any client. This is useful for external modules that don't have access to the client, but still want to define themselves as a handler. Example: >>> from telethon import events >>> @events.register(events.NewMessage) ... async def handler(event): ... ... ... >>> # (somewhere else) ... >>> from telethon import TelegramClient >>> client = TelegramClient(...) >>> client.add_event_handler(handler) Remember that you can use this as a non-decorator through ``register(event)(callback)``. Args: event (`_EventBuilder` | `type`): The event builder class or instance to be used, for instance ``events.NewMessage``. """ if isinstance(event, type): event = event() elif not event: event = Raw() def decorator(callback): handlers = getattr(callback, _HANDLERS_ATTRIBUTE, []) handlers.append(event) setattr(callback, _HANDLERS_ATTRIBUTE, handlers) return callback return decorator def unregister(callback, event=None): """ Inverse operation of `register` (though not a decorator). Client-less `remove_event_handler ` variant. **Note that this won't remove handlers from the client**, because it simply can't, so you would generally use this before adding the handlers to the client. This method is here for symmetry. You will rarely need to unregister events, since you can simply just not add them to any client. If no event is given, all events for this callback are removed. Returns how many callbacks were removed. """ found = 0 if event and not isinstance(event, type): event = type(event) handlers = getattr(callback, _HANDLERS_ATTRIBUTE, []) handlers.append((event, callback)) i = len(handlers) while i: i -= 1 ev = handlers[i] if not event or isinstance(ev, event): del handlers[i] found += 1 return found def is_handler(callback): """ Returns `True` if the given callback is an event handler (i.e. you used `register` on it). """ return hasattr(callback, _HANDLERS_ATTRIBUTE) def list(callback): """ Returns a list containing the registered event builders inside the specified callback handler. """ return getattr(callback, _HANDLERS_ATTRIBUTE, [])[:] def _get_handlers(callback): """ Like ``list`` but returns `None` if the callback was never registered. """ return getattr(callback, _HANDLERS_ATTRIBUTE, None) Telethon-1.39.0/telethon/events/album.py000066400000000000000000000311321475566265000202060ustar00rootroot00000000000000import asyncio import time import weakref from .common import EventBuilder, EventCommon, name_inner_event from .. import utils from ..tl import types from ..tl.custom.sendergetter import SenderGetter _IGNORE_MAX_SIZE = 100 # len() _IGNORE_MAX_AGE = 5 # seconds # IDs to ignore, and when they were added. If it grows too large, we will # remove old entries. Although it should generally not be bigger than 10, # it may be possible some updates are not processed and thus not removed. _IGNORE_DICT = {} _HACK_DELAY = 0.5 class AlbumHack: """ When receiving an album from a different data-center, they will come in separate `Updates`, so we need to temporarily remember them for a while and only after produce the event. Of course events are not designed for this kind of wizardy, so this is a dirty hack that gets the job done. When cleaning up the code base we may want to figure out a better way to do this, or just leave the album problem to the users; the update handling code is bad enough as it is. """ def __init__(self, client, event): # It's probably silly to use a weakref here because this object is # very short-lived but might as well try to do "the right thing". self._client = weakref.ref(client) self._event = event # parent event self._due = client.loop.time() + _HACK_DELAY client.loop.create_task(self.deliver_event()) def extend(self, messages): client = self._client() if client: # weakref may be dead self._event.messages.extend(messages) self._due = client.loop.time() + _HACK_DELAY async def deliver_event(self): while True: client = self._client() if client is None: return # weakref is dead, nothing to deliver diff = self._due - client.loop.time() if diff <= 0: # We've hit our due time, deliver event. It won't respect # sequential updates but fixing that would just worsen this. await client._dispatch_event(self._event) return del client # Clear ref and sleep until our due time await asyncio.sleep(diff) @name_inner_event class Album(EventBuilder): """ Occurs whenever you receive an album. This event only exists to ease dealing with an unknown amount of messages that belong to the same album. Example .. code-block:: python from telethon import events @client.on(events.Album) async def handler(event): # Counting how many photos or videos the album has print('Got an album with', len(event), 'items') # Forwarding the album as a whole to some chat event.forward_to(chat) # Printing the caption print(event.text) # Replying to the fifth item in the album await event.messages[4].reply('Cool!') """ def __init__( self, chats=None, *, blacklist_chats=False, func=None): super().__init__(chats, blacklist_chats=blacklist_chats, func=func) @classmethod def build(cls, update, others=None, self_id=None): # TODO normally we'd only check updates if they come with other updates # but MessageBox is not designed for this so others will always be None. # In essence we always rely on AlbumHack rather than returning early if not others. others = [update] if isinstance(update, (types.UpdateNewMessage, types.UpdateNewChannelMessage)): if not isinstance(update.message, types.Message): return # We don't care about MessageService's here group = update.message.grouped_id if group is None: return # It must be grouped # Check whether we are supposed to skip this update, and # if we do also remove it from the ignore list since we # won't need to check against it again. if _IGNORE_DICT.pop(id(update), None): return # Check if the ignore list is too big, and if it is clean it # TODO time could technically go backwards; time is not monotonic now = time.time() if len(_IGNORE_DICT) > _IGNORE_MAX_SIZE: for i in [i for i, t in _IGNORE_DICT.items() if now - t > _IGNORE_MAX_AGE]: del _IGNORE_DICT[i] # Add the other updates to the ignore list for u in others: if u is not update: _IGNORE_DICT[id(u)] = now # Figure out which updates share the same group and use those return cls.Event([ u.message for u in others if (isinstance(u, (types.UpdateNewMessage, types.UpdateNewChannelMessage)) and isinstance(u.message, types.Message) and u.message.grouped_id == group) ]) def filter(self, event): # Albums with less than two messages require a few hacks to work. if len(event.messages) > 1: return super().filter(event) class Event(EventCommon, SenderGetter): """ Represents the event of a new album. Members: messages (Sequence[`Message `]): The list of messages belonging to the same album. """ def __init__(self, messages): message = messages[0] super().__init__(chat_peer=message.peer_id, msg_id=message.id, broadcast=bool(message.post)) SenderGetter.__init__(self, message.sender_id) self.messages = messages def _set_client(self, client): super()._set_client(client) self._sender, self._input_sender = utils._get_entity_pair( self.sender_id, self._entities, client._mb_entity_cache) for msg in self.messages: msg._finish_init(client, self._entities, None) if len(self.messages) == 1: # This will require hacks to be a proper album event hack = client._albums.get(self.grouped_id) if hack is None: client._albums[self.grouped_id] = AlbumHack(client, self) else: hack.extend(self.messages) @property def grouped_id(self): """ The shared ``grouped_id`` between all the messages. """ return self.messages[0].grouped_id @property def text(self): """ The message text of the first photo with a caption, formatted using the client's default parse mode. """ return next((m.text for m in self.messages if m.text), '') @property def raw_text(self): """ The raw message text of the first photo with a caption, ignoring any formatting. """ return next((m.raw_text for m in self.messages if m.raw_text), '') @property def is_reply(self): """ `True` if the album is a reply to some other message. Remember that you can access the ID of the message this one is replying to through `reply_to_msg_id`, and the `Message` object with `get_reply_message()`. """ # Each individual message in an album all reply to the same message return self.messages[0].is_reply @property def forward(self): """ The `Forward ` information for the first message in the album if it was forwarded. """ # Each individual message in an album all reply to the same message return self.messages[0].forward # endregion Public Properties # region Public Methods async def get_reply_message(self): """ The `Message ` that this album is replying to, or `None`. The result will be cached after its first use. """ return await self.messages[0].get_reply_message() async def respond(self, *args, **kwargs): """ Responds to the album (not as a reply). Shorthand for `telethon.client.messages.MessageMethods.send_message` with ``entity`` already set. """ return await self.messages[0].respond(*args, **kwargs) async def reply(self, *args, **kwargs): """ Replies to the first photo in the album (as a reply). Shorthand for `telethon.client.messages.MessageMethods.send_message` with both ``entity`` and ``reply_to`` already set. """ return await self.messages[0].reply(*args, **kwargs) async def forward_to(self, *args, **kwargs): """ Forwards the entire album. Shorthand for `telethon.client.messages.MessageMethods.forward_messages` with both ``messages`` and ``from_peer`` already set. """ if self._client: kwargs['messages'] = self.messages kwargs['from_peer'] = await self.get_input_chat() return await self._client.forward_messages(*args, **kwargs) async def edit(self, *args, **kwargs): """ Edits the first caption or the message, or the first messages' caption if no caption is set, iff it's outgoing. Shorthand for `telethon.client.messages.MessageMethods.edit_message` with both ``entity`` and ``message`` already set. Returns `None` if the message was incoming, or the edited `Message` otherwise. .. note:: This is different from `client.edit_message ` and **will respect** the previous state of the message. For example, if the message didn't have a link preview, the edit won't add one by default, and you should force it by setting it to `True` if you want it. This is generally the most desired and convenient behaviour, and will work for link previews and message buttons. """ for msg in self.messages: if msg.raw_text: return await msg.edit(*args, **kwargs) return await self.messages[0].edit(*args, **kwargs) async def delete(self, *args, **kwargs): """ Deletes the entire album. You're responsible for checking whether you have the permission to do so, or to except the error otherwise. Shorthand for `telethon.client.messages.MessageMethods.delete_messages` with ``entity`` and ``message_ids`` already set. """ if self._client: return await self._client.delete_messages( await self.get_input_chat(), self.messages, *args, **kwargs ) async def mark_read(self): """ Marks the entire album as read. Shorthand for `client.send_read_acknowledge() ` with both ``entity`` and ``message`` already set. """ if self._client: await self._client.send_read_acknowledge( await self.get_input_chat(), max_id=self.messages[-1].id) async def pin(self, *, notify=False): """ Pins the first photo in the album. Shorthand for `telethon.client.messages.MessageMethods.pin_message` with both ``entity`` and ``message`` already set. """ return await self.messages[0].pin(notify=notify) def __len__(self): """ Return the amount of messages in the album. Equivalent to ``len(self.messages)``. """ return len(self.messages) def __iter__(self): """ Iterate over the messages in the album. Equivalent to ``iter(self.messages)``. """ return iter(self.messages) def __getitem__(self, n): """ Access the n'th message in the album. Equivalent to ``event.messages[n]``. """ return self.messages[n] Telethon-1.39.0/telethon/events/callbackquery.py000066400000000000000000000325231475566265000217350ustar00rootroot00000000000000import re import struct from .common import EventBuilder, EventCommon, name_inner_event from .. import utils from ..tl import types, functions from ..tl.custom.sendergetter import SenderGetter @name_inner_event class CallbackQuery(EventBuilder): """ Occurs whenever you sign in as a bot and a user clicks one of the inline buttons on your messages. Note that the `chats` parameter will **not** work with normal IDs or peers if the clicked inline button comes from a "via bot" message. The `chats` parameter also supports checking against the `chat_instance` which should be used for inline callbacks. Args: data (`bytes`, `str`, `callable`, optional): If set, the inline button payload data must match this data. A UTF-8 string can also be given, a regex or a callable. For instance, to check against ``'data_1'`` and ``'data_2'`` you can use ``re.compile(b'data_')``. pattern (`bytes`, `str`, `callable`, `Pattern`, optional): If set, only buttons with payload matching this pattern will be handled. You can specify a regex-like string which will be matched against the payload data, a callable function that returns `True` if a the payload data is acceptable, or a compiled regex pattern. Example .. code-block:: python from telethon import events, Button # Handle all callback queries and check data inside the handler @client.on(events.CallbackQuery) async def handler(event): if event.data == b'yes': await event.answer('Correct answer!') # Handle only callback queries with data being b'no' @client.on(events.CallbackQuery(data=b'no')) async def handler(event): # Pop-up message with alert await event.answer('Wrong answer!', alert=True) # Send a message with buttons users can click async def main(): await client.send_message(user, 'Yes or no?', buttons=[ Button.inline('Yes!', b'yes'), Button.inline('Nope', b'no') ]) """ def __init__( self, chats=None, *, blacklist_chats=False, func=None, data=None, pattern=None): super().__init__(chats, blacklist_chats=blacklist_chats, func=func) if data and pattern: raise ValueError("Only pass either data or pattern not both.") if isinstance(data, str): data = data.encode('utf-8') if isinstance(pattern, str): pattern = pattern.encode('utf-8') match = data if data else pattern if isinstance(match, bytes): self.match = data if data else re.compile(pattern).match elif not match or callable(match): self.match = match elif hasattr(match, 'match') and callable(match.match): if not isinstance(getattr(match, 'pattern', b''), bytes): match = re.compile(match.pattern.encode('utf-8'), match.flags & (~re.UNICODE)) self.match = match.match else: raise TypeError('Invalid data or pattern type given') self._no_check = all(x is None for x in ( self.chats, self.func, self.match, )) @classmethod def build(cls, update, others=None, self_id=None): if isinstance(update, types.UpdateBotCallbackQuery): return cls.Event(update, update.peer, update.msg_id) elif isinstance(update, types.UpdateInlineBotCallbackQuery): # See https://github.com/LonamiWebs/Telethon/pull/1005 # The long message ID is actually just msg_id + peer_id mid, pid = struct.unpack('`, since the message object is normally not present. """ self._client.loop.create_task(self.answer()) if isinstance(self.query.msg_id, (types.InputBotInlineMessageID, types.InputBotInlineMessageID64)): return await self._client.edit_message( self.query.msg_id, *args, **kwargs ) else: return await self._client.edit_message( await self.get_input_chat(), self.query.msg_id, *args, **kwargs ) async def delete(self, *args, **kwargs): """ Deletes the message. Shorthand for `telethon.client.messages.MessageMethods.delete_messages` with ``entity`` and ``message_ids`` already set. If you need to delete more than one message at once, don't use this `delete` method. Use a `telethon.client.telegramclient.TelegramClient` instance directly. This method also creates a task to `answer` the callback. This method will likely fail if `via_inline` is `True`. """ self._client.loop.create_task(self.answer()) if isinstance(self.query.msg_id, (types.InputBotInlineMessageID, types.InputBotInlineMessageID64)): raise TypeError('Inline messages cannot be deleted as there is no API request available to do so') return await self._client.delete_messages( await self.get_input_chat(), [self.query.msg_id], *args, **kwargs ) Telethon-1.39.0/telethon/events/chataction.py000066400000000000000000000430561475566265000212330ustar00rootroot00000000000000from .common import EventBuilder, EventCommon, name_inner_event from .. import utils from ..tl import types @name_inner_event class ChatAction(EventBuilder): """ Occurs on certain chat actions: * Whenever a new chat is created. * Whenever a chat's title or photo is changed or removed. * Whenever a new message is pinned. * Whenever a user scores in a game. * Whenever a user joins or is added to the group. * Whenever a user is removed or leaves a group if it has less than 50 members or the removed user was a bot. Note that "chat" refers to "small group, megagroup and broadcast channel", whereas "group" refers to "small group and megagroup" only. Example .. code-block:: python from telethon import events @client.on(events.ChatAction) async def handler(event): # Welcome every new user if event.user_joined: await event.reply('Welcome to the group!') """ @classmethod def build(cls, update, others=None, self_id=None): # Rely on specific pin updates for unpins, but otherwise ignore them # for new pins (we'd rather handle the new service message with pin, # so that we can act on that message'). if isinstance(update, types.UpdatePinnedChannelMessages) and not update.pinned: return cls.Event(types.PeerChannel(update.channel_id), pin_ids=update.messages, pin=update.pinned) elif isinstance(update, types.UpdatePinnedMessages) and not update.pinned: return cls.Event(update.peer, pin_ids=update.messages, pin=update.pinned) elif isinstance(update, types.UpdateChatParticipantAdd): return cls.Event(types.PeerChat(update.chat_id), added_by=update.inviter_id or True, users=update.user_id) elif isinstance(update, types.UpdateChatParticipantDelete): return cls.Event(types.PeerChat(update.chat_id), kicked_by=True, users=update.user_id) # UpdateChannel is sent if we leave a channel, and the update._entities # set by _process_update would let us make some guesses. However it's # better not to rely on this. Rely only in MessageActionChatDeleteUser. elif (isinstance(update, ( types.UpdateNewMessage, types.UpdateNewChannelMessage)) and isinstance(update.message, types.MessageService)): msg = update.message action = update.message.action if isinstance(action, types.MessageActionChatJoinedByLink): return cls.Event(msg, added_by=True, users=msg.from_id) elif isinstance(action, types.MessageActionChatAddUser): # If a user adds itself, it means they joined via the public chat username added_by = ([msg.sender_id] == action.users) or msg.from_id return cls.Event(msg, added_by=added_by, users=action.users) elif isinstance(action, types.MessageActionChatDeleteUser): return cls.Event(msg, kicked_by=utils.get_peer_id(msg.from_id) if msg.from_id else True, users=action.user_id) elif isinstance(action, types.MessageActionChatCreate): return cls.Event(msg, users=action.users, created=True, new_title=action.title) elif isinstance(action, types.MessageActionChannelCreate): return cls.Event(msg, created=True, users=msg.from_id, new_title=action.title) elif isinstance(action, types.MessageActionChatEditTitle): return cls.Event(msg, users=msg.from_id, new_title=action.title) elif isinstance(action, types.MessageActionChatEditPhoto): return cls.Event(msg, users=msg.from_id, new_photo=action.photo) elif isinstance(action, types.MessageActionChatDeletePhoto): return cls.Event(msg, users=msg.from_id, new_photo=True) elif isinstance(action, types.MessageActionPinMessage) and msg.reply_to: return cls.Event(msg, pin_ids=[msg.reply_to_msg_id]) elif isinstance(action, types.MessageActionGameScore): return cls.Event(msg, new_score=action.score) elif isinstance(update, types.UpdateChannelParticipant) \ and bool(update.new_participant) != bool(update.prev_participant): # If members are hidden, bots will receive this update instead, # as there won't be a service message. Promotions and demotions # seem to have both new and prev participant, which are ignored # by this event. return cls.Event(types.PeerChannel(update.channel_id), users=update.user_id, added_by=update.actor_id if update.new_participant else None, kicked_by=update.actor_id if update.prev_participant else None) class Event(EventCommon): """ Represents the event of a new chat action. Members: action_message (`MessageAction `_): The message invoked by this Chat Action. new_pin (`bool`): `True` if there is a new pin. new_photo (`bool`): `True` if there's a new chat photo (or it was removed). photo (:tl:`Photo`, optional): The new photo (or `None` if it was removed). user_added (`bool`): `True` if the user was added by some other. user_joined (`bool`): `True` if the user joined on their own. user_left (`bool`): `True` if the user left on their own. user_kicked (`bool`): `True` if the user was kicked by some other. created (`bool`, optional): `True` if this chat was just created. new_title (`str`, optional): The new title string for the chat, if applicable. new_score (`str`, optional): The new score string for the game, if applicable. unpin (`bool`): `True` if the existing pin gets unpinned. """ def __init__(self, where, new_photo=None, added_by=None, kicked_by=None, created=None, users=None, new_title=None, pin_ids=None, pin=None, new_score=None): if isinstance(where, types.MessageService): self.action_message = where where = where.peer_id else: self.action_message = None # TODO needs some testing (can there be more than one id, and do they follow pin order?) # same in get_pinned_message super().__init__(chat_peer=where, msg_id=pin_ids[0] if pin_ids else None) self.new_pin = pin_ids is not None self._pin_ids = pin_ids self._pinned_messages = None self.new_photo = new_photo is not None self.photo = \ new_photo if isinstance(new_photo, types.Photo) else None self._added_by = None self._kicked_by = None self.user_added = self.user_joined = self.user_left = \ self.user_kicked = self.unpin = False if added_by is True: self.user_joined = True elif added_by: self.user_added = True self._added_by = added_by # If `from_id` was not present (it's `True`) or the affected # user was "kicked by itself", then it left. Else it was kicked. if kicked_by is True or (users is not None and kicked_by == users): self.user_left = True elif kicked_by: self.user_kicked = True self._kicked_by = kicked_by self.created = bool(created) if isinstance(users, list): self._user_ids = [utils.get_peer_id(u) for u in users] elif users: self._user_ids = [utils.get_peer_id(users)] else: self._user_ids = [] self._users = None self._input_users = None self.new_title = new_title self.new_score = new_score self.unpin = not pin def _set_client(self, client): super()._set_client(client) if self.action_message: self.action_message._finish_init(client, self._entities, None) async def respond(self, *args, **kwargs): """ Responds to the chat action message (not as a reply). Shorthand for `telethon.client.messages.MessageMethods.send_message` with ``entity`` already set. """ return await self._client.send_message( await self.get_input_chat(), *args, **kwargs) async def reply(self, *args, **kwargs): """ Replies to the chat action message (as a reply). Shorthand for `telethon.client.messages.MessageMethods.send_message` with both ``entity`` and ``reply_to`` already set. Has the same effect as `respond` if there is no message. """ if not self.action_message: return await self.respond(*args, **kwargs) kwargs['reply_to'] = self.action_message.id return await self._client.send_message( await self.get_input_chat(), *args, **kwargs) async def delete(self, *args, **kwargs): """ Deletes the chat action message. You're responsible for checking whether you have the permission to do so, or to except the error otherwise. Shorthand for `telethon.client.messages.MessageMethods.delete_messages` with ``entity`` and ``message_ids`` already set. Does nothing if no message action triggered this event. """ if not self.action_message: return return await self._client.delete_messages( await self.get_input_chat(), [self.action_message], *args, **kwargs ) async def get_pinned_message(self): """ If ``new_pin`` is `True`, this returns the `Message ` object that was pinned. """ if self._pinned_messages is None: await self.get_pinned_messages() if self._pinned_messages: return self._pinned_messages[0] async def get_pinned_messages(self): """ If ``new_pin`` is `True`, this returns a `list` of `Message ` objects that were pinned. """ if not self._pin_ids: return self._pin_ids # either None or empty list chat = await self.get_input_chat() if chat: self._pinned_messages = await self._client.get_messages( self._input_chat, ids=self._pin_ids) return self._pinned_messages @property def added_by(self): """ The user who added ``users``, if applicable (`None` otherwise). """ if self._added_by and not isinstance(self._added_by, types.User): aby = self._entities.get(utils.get_peer_id(self._added_by)) if aby: self._added_by = aby return self._added_by async def get_added_by(self): """ Returns `added_by` but will make an API call if necessary. """ if not self.added_by and self._added_by: self._added_by = await self._client.get_entity(self._added_by) return self._added_by @property def kicked_by(self): """ The user who kicked ``users``, if applicable (`None` otherwise). """ if self._kicked_by and not isinstance(self._kicked_by, types.User): kby = self._entities.get(utils.get_peer_id(self._kicked_by)) if kby: self._kicked_by = kby return self._kicked_by async def get_kicked_by(self): """ Returns `kicked_by` but will make an API call if necessary. """ if not self.kicked_by and self._kicked_by: self._kicked_by = await self._client.get_entity(self._kicked_by) return self._kicked_by @property def user(self): """ The first user that takes part in this action. For example, who joined. Might be `None` if the information can't be retrieved or there is no user taking part. """ if self.users: return self._users[0] async def get_user(self): """ Returns `user` but will make an API call if necessary. """ if self.users or await self.get_users(): return self._users[0] @property def input_user(self): """ Input version of the ``self.user`` property. """ if self.input_users: return self._input_users[0] async def get_input_user(self): """ Returns `input_user` but will make an API call if necessary. """ if self.input_users or await self.get_input_users(): return self._input_users[0] @property def user_id(self): """ Returns the marked signed ID of the first user, if any. """ if self._user_ids: return self._user_ids[0] @property def users(self): """ A list of users that take part in this action. For example, who joined. Might be empty if the information can't be retrieved or there are no users taking part. """ if not self._user_ids: return [] if self._users is None: self._users = [ self._entities[user_id] for user_id in self._user_ids if user_id in self._entities ] return self._users async def get_users(self): """ Returns `users` but will make an API call if necessary. """ if not self._user_ids: return [] # Note: we access the property first so that it fills if needed if (self.users is None or len(self._users) != len(self._user_ids)) and self.action_message: await self.action_message._reload_message() self._users = [ u for u in self.action_message.action_entities if isinstance(u, (types.User, types.UserEmpty))] return self._users @property def input_users(self): """ Input version of the ``self.users`` property. """ if self._input_users is None and self._user_ids: self._input_users = [] for user_id in self._user_ids: # First try to get it from our entities try: self._input_users.append(utils.get_input_peer(self._entities[user_id])) continue except (KeyError, TypeError): pass # If missing, try from the entity cache try: self._input_users.append(self._client._mb_entity_cache.get( utils.resolve_id(user_id)[0])._as_input_peer()) continue except AttributeError: pass return self._input_users or [] async def get_input_users(self): """ Returns `input_users` but will make an API call if necessary. """ if not self._user_ids: return [] # Note: we access the property first so that it fills if needed if (self.input_users is None or len(self._input_users) != len(self._user_ids)) and self.action_message: self._input_users = [ utils.get_input_peer(u) for u in self.action_message.action_entities if isinstance(u, (types.User, types.UserEmpty))] return self._input_users or [] @property def user_ids(self): """ Returns the marked signed ID of the users, if any. """ if self._user_ids: return self._user_ids[:] Telethon-1.39.0/telethon/events/common.py000066400000000000000000000142531475566265000204030ustar00rootroot00000000000000import abc import asyncio import warnings from .. import utils from ..tl import TLObject, types from ..tl.custom.chatgetter import ChatGetter async def _into_id_set(client, chats): """Helper util to turn the input chat or chats into a set of IDs.""" if chats is None: return None if not utils.is_list_like(chats): chats = (chats,) result = set() for chat in chats: if isinstance(chat, int): if chat < 0: result.add(chat) # Explicitly marked IDs are negative else: result.update({ # Support all valid types of peers utils.get_peer_id(types.PeerUser(chat)), utils.get_peer_id(types.PeerChat(chat)), utils.get_peer_id(types.PeerChannel(chat)), }) elif isinstance(chat, TLObject) and chat.SUBCLASS_OF_ID == 0x2d45687: # 0x2d45687 == crc32(b'Peer') result.add(utils.get_peer_id(chat)) else: chat = await client.get_input_entity(chat) if isinstance(chat, types.InputPeerSelf): chat = await client.get_me(input_peer=True) result.add(utils.get_peer_id(chat)) return result class EventBuilder(abc.ABC): """ The common event builder, with builtin support to filter per chat. Args: chats (`entity`, optional): May be one or more entities (username/peer/etc.), preferably IDs. By default, only matching chats will be handled. blacklist_chats (`bool`, optional): Whether to treat the chats as a blacklist instead of as a whitelist (default). This means that every chat will be handled *except* those specified in ``chats`` which will be ignored if ``blacklist_chats=True``. func (`callable`, optional): A callable (async or not) function that should accept the event as input parameter, and return a value indicating whether the event should be dispatched or not (any truthy value will do, it does not need to be a `bool`). It works like a custom filter: .. code-block:: python @client.on(events.NewMessage(func=lambda e: e.is_private)) async def handler(event): pass # code here """ def __init__(self, chats=None, *, blacklist_chats=False, func=None): self.chats = chats self.blacklist_chats = bool(blacklist_chats) self.resolved = False self.func = func self._resolve_lock = None @classmethod @abc.abstractmethod def build(cls, update, others=None, self_id=None): """ Builds an event for the given update if possible, or returns None. `others` are the rest of updates that came in the same container as the current `update`. `self_id` should be the current user's ID, since it is required for some events which lack this information but still need it. """ # TODO So many parameters specific to only some update types seems dirty async def resolve(self, client): """Helper method to allow event builders to be resolved before usage""" if self.resolved: return if not self._resolve_lock: self._resolve_lock = asyncio.Lock() async with self._resolve_lock: if not self.resolved: await self._resolve(client) self.resolved = True async def _resolve(self, client): self.chats = await _into_id_set(client, self.chats) def filter(self, event): """ Returns a truthy value if the event passed the filter and should be used, or falsy otherwise. The return value may need to be awaited. The events must have been resolved before this can be called. """ if not self.resolved: return if self.chats is not None: # Note: the `event.chat_id` property checks if it's `None` for us inside = event.chat_id in self.chats if inside == self.blacklist_chats: # If this chat matches but it's a blacklist ignore. # If it doesn't match but it's a whitelist ignore. return if not self.func: return True # Return the result of func directly as it may need to be awaited return self.func(event) class EventCommon(ChatGetter, abc.ABC): """ Intermediate class with common things to all events. Remember that this class implements `ChatGetter ` which means you have access to all chat properties and methods. In addition, you can access the `original_update` field which contains the original :tl:`Update`. """ _event_name = 'Event' def __init__(self, chat_peer=None, msg_id=None, broadcast=None): super().__init__(chat_peer, broadcast=broadcast) self._entities = {} self._client = None self._message_id = msg_id self.original_update = None def _set_client(self, client): """ Setter so subclasses can act accordingly when the client is set. """ self._client = client if self._chat_peer: self._chat, self._input_chat = utils._get_entity_pair( self.chat_id, self._entities, client._mb_entity_cache) else: self._chat = self._input_chat = None @property def client(self): """ The `telethon.TelegramClient` that created this event. """ return self._client def __str__(self): return TLObject.pretty_format(self.to_dict()) def stringify(self): return TLObject.pretty_format(self.to_dict(), indent=0) def to_dict(self): d = {k: v for k, v in self.__dict__.items() if k[0] != '_'} d['_'] = self._event_name return d def name_inner_event(cls): """Decorator to rename cls.Event 'Event' as 'cls.Event'""" if hasattr(cls, 'Event'): cls.Event._event_name = '{}.Event'.format(cls.__name__) else: warnings.warn('Class {} does not have a inner Event'.format(cls)) return cls Telethon-1.39.0/telethon/events/inlinequery.py000066400000000000000000000214161475566265000214560ustar00rootroot00000000000000import inspect import re import asyncio from .common import EventBuilder, EventCommon, name_inner_event from .. import utils, helpers from ..tl import types, functions, custom from ..tl.custom.sendergetter import SenderGetter @name_inner_event class InlineQuery(EventBuilder): """ Occurs whenever you sign in as a bot and a user sends an inline query such as ``@bot query``. Args: users (`entity`, optional): May be one or more entities (username/peer/etc.), preferably IDs. By default, only inline queries from these users will be handled. blacklist_users (`bool`, optional): Whether to treat the users as a blacklist instead of as a whitelist (default). This means that every chat will be handled *except* those specified in ``users`` which will be ignored if ``blacklist_users=True``. pattern (`str`, `callable`, `Pattern`, optional): If set, only queries matching this pattern will be handled. You can specify a regex-like string which will be matched against the message, a callable function that returns `True` if a message is acceptable, or a compiled regex pattern. Example .. code-block:: python from telethon import events @client.on(events.InlineQuery) async def handler(event): builder = event.builder # Two options (convert user text to UPPERCASE or lowercase) await event.answer([ builder.article('UPPERCASE', text=event.text.upper()), builder.article('lowercase', text=event.text.lower()), ]) """ def __init__( self, users=None, *, blacklist_users=False, func=None, pattern=None): super().__init__(users, blacklist_chats=blacklist_users, func=func) if isinstance(pattern, str): self.pattern = re.compile(pattern).match elif not pattern or callable(pattern): self.pattern = pattern elif hasattr(pattern, 'match') and callable(pattern.match): self.pattern = pattern.match else: raise TypeError('Invalid pattern type given') @classmethod def build(cls, update, others=None, self_id=None): if isinstance(update, types.UpdateBotInlineQuery): return cls.Event(update) def filter(self, event): if self.pattern: match = self.pattern(event.text) if not match: return event.pattern_match = match return super().filter(event) class Event(EventCommon, SenderGetter): """ Represents the event of a new callback query. Members: query (:tl:`UpdateBotInlineQuery`): The original :tl:`UpdateBotInlineQuery`. Make sure to access the `text` property of the query if you want the text rather than the actual query object. pattern_match (`obj`, optional): The resulting object from calling the passed ``pattern`` function, which is ``re.compile(...).match`` by default. """ def __init__(self, query): super().__init__(chat_peer=types.PeerUser(query.user_id)) SenderGetter.__init__(self, query.user_id) self.query = query self.pattern_match = None self._answered = False def _set_client(self, client): super()._set_client(client) self._sender, self._input_sender = utils._get_entity_pair( self.sender_id, self._entities, client._mb_entity_cache) @property def id(self): """ Returns the unique identifier for the query ID. """ return self.query.query_id @property def text(self): """ Returns the text the user used to make the inline query. """ return self.query.query @property def offset(self): """ The string the user's client used as an offset for the query. This will either be empty or equal to offsets passed to `answer`. """ return self.query.offset @property def geo(self): """ If the user location is requested when using inline mode and the user's device is able to send it, this will return the :tl:`GeoPoint` with the position of the user. """ return self.query.geo @property def builder(self): """ Returns a new `InlineBuilder ` instance. """ return custom.InlineBuilder(self._client) async def answer( self, results=None, cache_time=0, *, gallery=False, next_offset=None, private=False, switch_pm=None, switch_pm_param=''): """ Answers the inline query with the given results. See the documentation for `builder` to know what kind of answers can be given. Args: results (`list`, optional): A list of :tl:`InputBotInlineResult` to use. You should use `builder` to create these: .. code-block:: python builder = inline.builder r1 = builder.article('Be nice', text='Have a nice day') r2 = builder.article('Be bad', text="I don't like you") await inline.answer([r1, r2]) You can send up to 50 results as documented in https://core.telegram.org/bots/api#answerinlinequery. Sending more will raise ``ResultsTooMuchError``, and you should consider using `next_offset` to paginate them. cache_time (`int`, optional): For how long this result should be cached on the user's client. Defaults to 0 for no cache. gallery (`bool`, optional): Whether the results should show as a gallery (grid) or not. next_offset (`str`, optional): The offset the client will send when the user scrolls the results and it repeats the request. private (`bool`, optional): Whether the results should be cached by Telegram (not private) or by the user's client (private). switch_pm (`str`, optional): If set, this text will be shown in the results to allow the user to switch to private messages. switch_pm_param (`str`, optional): Optional parameter to start the bot with if `switch_pm` was used. Example: .. code-block:: python @bot.on(events.InlineQuery) async def handler(event): builder = event.builder rev_text = event.text[::-1] await event.answer([ builder.article('Reverse text', text=rev_text), builder.photo('/path/to/photo.jpg') ]) """ if self._answered: return if results: futures = [self._as_future(x) for x in results] await asyncio.wait(futures) # All futures will be in the `done` *set* that `wait` returns. # # Precisely because it's a `set` and not a `list`, it # will not preserve the order, but since all futures # completed we can use our original, ordered `list`. results = [x.result() for x in futures] else: results = [] if switch_pm: switch_pm = types.InlineBotSwitchPM(switch_pm, switch_pm_param) return await self._client( functions.messages.SetInlineBotResultsRequest( query_id=self.query.query_id, results=results, cache_time=cache_time, gallery=gallery, next_offset=next_offset, private=private, switch_pm=switch_pm ) ) @staticmethod def _as_future(obj): if inspect.isawaitable(obj): return asyncio.ensure_future(obj) f = helpers.get_running_loop().create_future() f.set_result(obj) return f Telethon-1.39.0/telethon/events/messagedeleted.py000066400000000000000000000041201475566265000220560ustar00rootroot00000000000000from .common import EventBuilder, EventCommon, name_inner_event from ..tl import types @name_inner_event class MessageDeleted(EventBuilder): """ Occurs whenever a message is deleted. Note that this event isn't 100% reliable, since Telegram doesn't always notify the clients that a message was deleted. .. important:: Telegram **does not** send information about *where* a message was deleted if it occurs in private conversations with other users or in small group chats, because message IDs are *unique* and you can identify the chat with the message ID alone if you saved it previously. Telethon **does not** save information of where messages occur, so it cannot know in which chat a message was deleted (this will only work in channels, where the channel ID *is* present). This means that the ``chats=`` parameter will not work reliably, unless you intend on working with channels and super-groups only. Example .. code-block:: python from telethon import events @client.on(events.MessageDeleted) async def handler(event): # Log all deleted message IDs for msg_id in event.deleted_ids: print('Message', msg_id, 'was deleted in', event.chat_id) """ @classmethod def build(cls, update, others=None, self_id=None): if isinstance(update, types.UpdateDeleteMessages): return cls.Event( deleted_ids=update.messages, peer=None ) elif isinstance(update, types.UpdateDeleteChannelMessages): return cls.Event( deleted_ids=update.messages, peer=types.PeerChannel(update.channel_id) ) class Event(EventCommon): def __init__(self, deleted_ids, peer): super().__init__( chat_peer=peer, msg_id=(deleted_ids or [0])[0] ) self.deleted_id = None if not deleted_ids else deleted_ids[0] self.deleted_ids = deleted_ids Telethon-1.39.0/telethon/events/messageedited.py000066400000000000000000000035361475566265000217200ustar00rootroot00000000000000from .common import name_inner_event from .newmessage import NewMessage from ..tl import types @name_inner_event class MessageEdited(NewMessage): """ Occurs whenever a message is edited. Just like `NewMessage `, you should treat this event as a `Message `. .. warning:: On channels, `Message.out ` will be `True` if you sent the message originally, **not if you edited it**! This can be dangerous if you run outgoing commands on edits. Some examples follow: * You send a message "A", ``out is True``. * You edit "A" to "B", ``out is True``. * Someone else edits "B" to "C", ``out is True`` (**be careful!**). * Someone sends "X", ``out is False``. * Someone edits "X" to "Y", ``out is False``. * You edit "Y" to "Z", ``out is False``. Since there are useful cases where you need the right ``out`` value, the library cannot do anything automatically to help you. Instead, consider using ``from_users='me'`` (it won't work in broadcast channels at all since the sender is the channel and not you). Example .. code-block:: python from telethon import events @client.on(events.MessageEdited) async def handler(event): # Log the date of new edits print('Message', event.id, 'changed at', event.date) """ @classmethod def build(cls, update, others=None, self_id=None): if isinstance(update, (types.UpdateEditMessage, types.UpdateEditChannelMessage)): return cls.Event(update.message) class Event(NewMessage.Event): pass # Required if we want a different name for it Telethon-1.39.0/telethon/events/messageread.py000066400000000000000000000125361475566265000213750ustar00rootroot00000000000000from .common import EventBuilder, EventCommon, name_inner_event from .. import utils from ..tl import types @name_inner_event class MessageRead(EventBuilder): """ Occurs whenever one or more messages are read in a chat. Args: inbox (`bool`, optional): If this argument is `True`, then when you read someone else's messages the event will be fired. By default (`False`) only when messages you sent are read by someone else will fire it. Example .. code-block:: python from telethon import events @client.on(events.MessageRead) async def handler(event): # Log when someone reads your messages print('Someone has read all your messages until', event.max_id) @client.on(events.MessageRead(inbox=True)) async def handler(event): # Log when you read message in a chat (from your "inbox") print('You have read messages until', event.max_id) """ def __init__( self, chats=None, *, blacklist_chats=False, func=None, inbox=False): super().__init__(chats, blacklist_chats=blacklist_chats, func=func) self.inbox = inbox @classmethod def build(cls, update, others=None, self_id=None): if isinstance(update, types.UpdateReadHistoryInbox): return cls.Event(update.peer, update.max_id, False) elif isinstance(update, types.UpdateReadHistoryOutbox): return cls.Event(update.peer, update.max_id, True) elif isinstance(update, types.UpdateReadChannelInbox): return cls.Event(types.PeerChannel(update.channel_id), update.max_id, False) elif isinstance(update, types.UpdateReadChannelOutbox): return cls.Event(types.PeerChannel(update.channel_id), update.max_id, True) elif isinstance(update, types.UpdateReadMessagesContents): return cls.Event(message_ids=update.messages, contents=True) elif isinstance(update, types.UpdateChannelReadMessagesContents): return cls.Event(types.PeerChannel(update.channel_id), message_ids=update.messages, contents=True) def filter(self, event): if self.inbox == event.outbox: return return super().filter(event) class Event(EventCommon): """ Represents the event of one or more messages being read. Members: max_id (`int`): Up to which message ID has been read. Every message with an ID equal or lower to it have been read. outbox (`bool`): `True` if someone else has read your messages. contents (`bool`): `True` if what was read were the contents of a message. This will be the case when e.g. you play a voice note. It may only be set on ``inbox`` events. """ def __init__(self, peer=None, max_id=None, out=False, contents=False, message_ids=None): self.outbox = out self.contents = contents self._message_ids = message_ids or [] self._messages = None self.max_id = max_id or max(message_ids or [], default=None) super().__init__(peer, self.max_id) @property def inbox(self): """ `True` if you have read someone else's messages. """ return not self.outbox @property def message_ids(self): """ The IDs of the messages **which contents'** were read. Use :meth:`is_read` if you need to check whether a message was read instead checking if it's in here. """ return self._message_ids async def get_messages(self): """ Returns the list of `Message ` **which contents'** were read. Use :meth:`is_read` if you need to check whether a message was read instead checking if it's in here. """ if self._messages is None: chat = await self.get_input_chat() if not chat: self._messages = [] else: self._messages = await self._client.get_messages( chat, ids=self._message_ids) return self._messages def is_read(self, message): """ Returns `True` if the given message (or its ID) has been read. If a list-like argument is provided, this method will return a list of booleans indicating which messages have been read. """ if utils.is_list_like(message): return [(m if isinstance(m, int) else m.id) <= self.max_id for m in message] else: return (message if isinstance(message, int) else message.id) <= self.max_id def __contains__(self, message): """`True` if the message(s) are read message.""" if utils.is_list_like(message): return all(self.is_read(message)) else: return self.is_read(message) Telethon-1.39.0/telethon/events/newmessage.py000066400000000000000000000217111475566265000212460ustar00rootroot00000000000000import re from .common import EventBuilder, EventCommon, name_inner_event, _into_id_set from .. import utils from ..tl import types @name_inner_event class NewMessage(EventBuilder): """ Occurs whenever a new text message or a message with media arrives. Args: incoming (`bool`, optional): If set to `True`, only **incoming** messages will be handled. Mutually exclusive with ``outgoing`` (can only set one of either). outgoing (`bool`, optional): If set to `True`, only **outgoing** messages will be handled. Mutually exclusive with ``incoming`` (can only set one of either). from_users (`entity`, optional): Unlike `chats`, this parameter filters the *senders* of the message. That is, only messages *sent by these users* will be handled. Use `chats` if you want private messages with this/these users. `from_users` lets you filter by messages sent by *one or more* users across the desired chats (doesn't need a list). forwards (`bool`, optional): Whether forwarded messages should be handled or not. By default, both forwarded and normal messages are included. If it's `True` *only* forwards will be handled. If it's `False` only messages that are *not* forwards will be handled. pattern (`str`, `callable`, `Pattern`, optional): If set, only messages matching this pattern will be handled. You can specify a regex-like string which will be matched against the message, a callable function that returns `True` if a message is acceptable, or a compiled regex pattern. Example .. code-block:: python import asyncio from telethon import events @client.on(events.NewMessage(pattern='(?i)hello.+')) async def handler(event): # Respond whenever someone says "Hello" and something else await event.reply('Hey!') @client.on(events.NewMessage(outgoing=True, pattern='!ping')) async def handler(event): # Say "!pong" whenever you send "!ping", then delete both messages m = await event.respond('!pong') await asyncio.sleep(5) await client.delete_messages(event.chat_id, [event.id, m.id]) """ def __init__(self, chats=None, *, blacklist_chats=False, func=None, incoming=None, outgoing=None, from_users=None, forwards=None, pattern=None): if incoming and outgoing: incoming = outgoing = None # Same as no filter elif incoming is not None and outgoing is None: outgoing = not incoming elif outgoing is not None and incoming is None: incoming = not outgoing elif all(x is not None and not x for x in (incoming, outgoing)): raise ValueError("Don't create an event handler if you " "don't want neither incoming nor outgoing!") super().__init__(chats, blacklist_chats=blacklist_chats, func=func) self.incoming = incoming self.outgoing = outgoing self.from_users = from_users self.forwards = forwards if isinstance(pattern, str): self.pattern = re.compile(pattern).match elif not pattern or callable(pattern): self.pattern = pattern elif hasattr(pattern, 'match') and callable(pattern.match): self.pattern = pattern.match else: raise TypeError('Invalid pattern type given') # Should we short-circuit? E.g. perform no check at all self._no_check = all(x is None for x in ( self.chats, self.incoming, self.outgoing, self.pattern, self.from_users, self.forwards, self.from_users, self.func )) async def _resolve(self, client): await super()._resolve(client) self.from_users = await _into_id_set(client, self.from_users) @classmethod def build(cls, update, others=None, self_id=None): if isinstance(update, (types.UpdateNewMessage, types.UpdateNewChannelMessage)): if not isinstance(update.message, types.Message): return # We don't care about MessageService's here event = cls.Event(update.message) elif isinstance(update, types.UpdateShortMessage): event = cls.Event(types.Message( out=update.out, mentioned=update.mentioned, media_unread=update.media_unread, silent=update.silent, id=update.id, peer_id=types.PeerUser(update.user_id), from_id=types.PeerUser(self_id if update.out else update.user_id), message=update.message, date=update.date, fwd_from=update.fwd_from, via_bot_id=update.via_bot_id, reply_to=update.reply_to, entities=update.entities, ttl_period=update.ttl_period )) elif isinstance(update, types.UpdateShortChatMessage): event = cls.Event(types.Message( out=update.out, mentioned=update.mentioned, media_unread=update.media_unread, silent=update.silent, id=update.id, from_id=types.PeerUser(self_id if update.out else update.from_id), peer_id=types.PeerChat(update.chat_id), message=update.message, date=update.date, fwd_from=update.fwd_from, via_bot_id=update.via_bot_id, reply_to=update.reply_to, entities=update.entities, ttl_period=update.ttl_period )) else: return return event def filter(self, event): if self._no_check: return event if self.incoming and event.message.out: return if self.outgoing and not event.message.out: return if self.forwards is not None: if bool(self.forwards) != bool(event.message.fwd_from): return if self.from_users is not None: if event.message.sender_id not in self.from_users: return if self.pattern: match = self.pattern(event.message.message or '') if not match: return event.pattern_match = match return super().filter(event) class Event(EventCommon): """ Represents the event of a new message. This event can be treated to all effects as a `Message `, so please **refer to its documentation** to know what you can do with this event. Members: message (`Message `): This is the only difference with the received `Message `, and will return the `telethon.tl.custom.message.Message` itself, not the text. See `Message ` for the rest of available members and methods. pattern_match (`obj`): The resulting object from calling the passed ``pattern`` function. Here's an example using a string (defaults to regex match): >>> from telethon import TelegramClient, events >>> client = TelegramClient(...) >>> >>> @client.on(events.NewMessage(pattern=r'hi (\\w+)!')) ... async def handler(event): ... # In this case, the result is a ``Match`` object ... # since the `str` pattern was converted into ... # the ``re.compile(pattern).match`` function. ... print('Welcomed', event.pattern_match.group(1)) ... >>> """ def __init__(self, message): self.__dict__['_init'] = False super().__init__(chat_peer=message.peer_id, msg_id=message.id, broadcast=bool(message.post)) self.pattern_match = None self.message = message def _set_client(self, client): super()._set_client(client) m = self.message m._finish_init(client, self._entities, None) self.__dict__['_init'] = True # No new attributes can be set def __getattr__(self, item): if item in self.__dict__: return self.__dict__[item] else: return getattr(self.message, item) def __setattr__(self, name, value): if not self.__dict__['_init'] or name in self.__dict__: self.__dict__[name] = value else: setattr(self.message, name, value) Telethon-1.39.0/telethon/events/raw.py000066400000000000000000000031631475566265000177020ustar00rootroot00000000000000from .common import EventBuilder from .. import utils class Raw(EventBuilder): """ Raw events are not actual events. Instead, they are the raw :tl:`Update` object that Telegram sends. You normally shouldn't need these. Args: types (`list` | `tuple` | `type`, optional): The type or types that the :tl:`Update` instance must be. Equivalent to ``if not isinstance(update, types): return``. Example .. code-block:: python from telethon import events @client.on(events.Raw) async def handler(update): # Print all incoming updates print(update.stringify()) """ def __init__(self, types=None, *, func=None): super().__init__(func=func) if not types: self.types = None elif not utils.is_list_like(types): if not isinstance(types, type): raise TypeError('Invalid input type given: {}'.format(types)) self.types = types else: if not all(isinstance(x, type) for x in types): raise TypeError('Invalid input types given: {}'.format(types)) self.types = tuple(types) async def resolve(self, client): self.resolved = True @classmethod def build(cls, update, others=None, self_id=None): return update def filter(self, event): if not self.types or isinstance(event, self.types): if self.func: # Return the result of func directly as it may need to be awaited return self.func(event) return event Telethon-1.39.0/telethon/events/userupdate.py000066400000000000000000000245741475566265000213030ustar00rootroot00000000000000import datetime import functools from .common import EventBuilder, EventCommon, name_inner_event from .. import utils from ..tl import types from ..tl.custom.sendergetter import SenderGetter # TODO Either the properties are poorly named or they should be # different events, but that would be a breaking change. # # TODO There are more "user updates", but bundling them all up # in a single place will make it annoying to use (since # the user needs to check for the existence of `None`). # # TODO Handle UpdateUserBlocked, UpdateUserName, UpdateUserPhone, UpdateUser def _requires_action(function): @functools.wraps(function) def wrapped(self): return None if self.action is None else function(self) return wrapped def _requires_status(function): @functools.wraps(function) def wrapped(self): return None if self.status is None else function(self) return wrapped @name_inner_event class UserUpdate(EventBuilder): """ Occurs whenever a user goes online, starts typing, etc. Example .. code-block:: python from telethon import events @client.on(events.UserUpdate) async def handler(event): # If someone is uploading, say something if event.uploading: await client.send_message(event.user_id, 'What are you sending?') """ @classmethod def build(cls, update, others=None, self_id=None): if isinstance(update, types.UpdateUserStatus): return cls.Event(types.PeerUser(update.user_id), status=update.status) elif isinstance(update, types.UpdateChannelUserTyping): return cls.Event(update.from_id, chat_peer=types.PeerChannel(update.channel_id), typing=update.action) elif isinstance(update, types.UpdateChatUserTyping): return cls.Event(update.from_id, chat_peer=types.PeerChat(update.chat_id), typing=update.action) elif isinstance(update, types.UpdateUserTyping): return cls.Event(update.user_id, typing=update.action) class Event(EventCommon, SenderGetter): """ Represents the event of a user update such as gone online, started typing, etc. Members: status (:tl:`UserStatus`, optional): The user status if the update is about going online or offline. You should check this attribute first before checking any of the seen within properties, since they will all be `None` if the status is not set. action (:tl:`SendMessageAction`, optional): The "typing" action if any the user is performing if any. You should check this attribute first before checking any of the typing properties, since they will all be `None` if the action is not set. """ def __init__(self, peer, *, status=None, chat_peer=None, typing=None): super().__init__(chat_peer or peer) SenderGetter.__init__(self, utils.get_peer_id(peer)) self.status = status self.action = typing def _set_client(self, client): super()._set_client(client) self._sender, self._input_sender = utils._get_entity_pair( self.sender_id, self._entities, client._mb_entity_cache) @property def user(self): """Alias for `sender `.""" return self.sender async def get_user(self): """Alias for `get_sender `.""" return await self.get_sender() @property def input_user(self): """Alias for `input_sender `.""" return self.input_sender async def get_input_user(self): """Alias for `get_input_sender `.""" return await self.get_input_sender() @property def user_id(self): """Alias for `sender_id `.""" return self.sender_id @property @_requires_action def typing(self): """ `True` if the action is typing a message. """ return isinstance(self.action, types.SendMessageTypingAction) @property @_requires_action def uploading(self): """ `True` if the action is uploading something. """ return isinstance(self.action, ( types.SendMessageChooseContactAction, types.SendMessageChooseStickerAction, types.SendMessageUploadAudioAction, types.SendMessageUploadDocumentAction, types.SendMessageUploadPhotoAction, types.SendMessageUploadRoundAction, types.SendMessageUploadVideoAction )) @property @_requires_action def recording(self): """ `True` if the action is recording something. """ return isinstance(self.action, ( types.SendMessageRecordAudioAction, types.SendMessageRecordRoundAction, types.SendMessageRecordVideoAction )) @property @_requires_action def playing(self): """ `True` if the action is playing a game. """ return isinstance(self.action, types.SendMessageGamePlayAction) @property @_requires_action def cancel(self): """ `True` if the action was cancelling other actions. """ return isinstance(self.action, types.SendMessageCancelAction) @property @_requires_action def geo(self): """ `True` if what's being uploaded is a geo. """ return isinstance(self.action, types.SendMessageGeoLocationAction) @property @_requires_action def audio(self): """ `True` if what's being recorded/uploaded is an audio. """ return isinstance(self.action, ( types.SendMessageRecordAudioAction, types.SendMessageUploadAudioAction )) @property @_requires_action def round(self): """ `True` if what's being recorded/uploaded is a round video. """ return isinstance(self.action, ( types.SendMessageRecordRoundAction, types.SendMessageUploadRoundAction )) @property @_requires_action def video(self): """ `True` if what's being recorded/uploaded is an video. """ return isinstance(self.action, ( types.SendMessageRecordVideoAction, types.SendMessageUploadVideoAction )) @property @_requires_action def contact(self): """ `True` if what's being uploaded (selected) is a contact. """ return isinstance(self.action, types.SendMessageChooseContactAction) @property @_requires_action def document(self): """ `True` if what's being uploaded is document. """ return isinstance(self.action, types.SendMessageUploadDocumentAction) @property @_requires_action def sticker(self): """ `True` if what's being uploaded is a sticker. """ return isinstance(self.action, types.SendMessageChooseStickerAction) @property @_requires_action def photo(self): """ `True` if what's being uploaded is a photo. """ return isinstance(self.action, types.SendMessageUploadPhotoAction) @property @_requires_status def last_seen(self): """ Exact `datetime.datetime` when the user was last seen if known. """ if isinstance(self.status, types.UserStatusOffline): return self.status.was_online @property @_requires_status def until(self): """ The `datetime.datetime` until when the user should appear online. """ if isinstance(self.status, types.UserStatusOnline): return self.status.expires def _last_seen_delta(self): if isinstance(self.status, types.UserStatusOffline): return datetime.datetime.now(tz=datetime.timezone.utc) - self.status.was_online elif isinstance(self.status, types.UserStatusOnline): return datetime.timedelta(days=0) elif isinstance(self.status, types.UserStatusRecently): return datetime.timedelta(days=1) elif isinstance(self.status, types.UserStatusLastWeek): return datetime.timedelta(days=7) elif isinstance(self.status, types.UserStatusLastMonth): return datetime.timedelta(days=30) else: return datetime.timedelta(days=365) @property @_requires_status def online(self): """ `True` if the user is currently online, """ return self._last_seen_delta() <= datetime.timedelta(days=0) @property @_requires_status def recently(self): """ `True` if the user was seen within a day. """ return self._last_seen_delta() <= datetime.timedelta(days=1) @property @_requires_status def within_weeks(self): """ `True` if the user was seen within 7 days. """ return self._last_seen_delta() <= datetime.timedelta(days=7) @property @_requires_status def within_months(self): """ `True` if the user was seen within 30 days. """ return self._last_seen_delta() <= datetime.timedelta(days=30) Telethon-1.39.0/telethon/extensions/000077500000000000000000000000001475566265000174275ustar00rootroot00000000000000Telethon-1.39.0/telethon/extensions/__init__.py000066400000000000000000000004301475566265000215350ustar00rootroot00000000000000""" Several extensions Python is missing, such as a proper class to handle a TCP communication with support for cancelling the operation, and a utility class to read arbitrary binary data in a more comfortable way, with int/strings/etc. """ from .binaryreader import BinaryReader Telethon-1.39.0/telethon/extensions/binaryreader.py000066400000000000000000000131611475566265000224520ustar00rootroot00000000000000""" This module contains the BinaryReader utility class. """ import os import time from datetime import datetime, timezone, timedelta from io import BytesIO from struct import unpack from ..errors import TypeNotFoundError from ..tl.alltlobjects import tlobjects from ..tl.core import core_objects _EPOCH_NAIVE = datetime(*time.gmtime(0)[:6]) _EPOCH = _EPOCH_NAIVE.replace(tzinfo=timezone.utc) class BinaryReader: """ Small utility class to read binary data. """ def __init__(self, data): self.stream = BytesIO(data) self._last = None # Should come in handy to spot -404 errors # region Reading # "All numbers are written as little endian." # https://core.telegram.org/mtproto def read_byte(self): """Reads a single byte value.""" return self.read(1)[0] def read_int(self, signed=True): """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.""" return int.from_bytes(self.read(8), byteorder='little', signed=signed) def read_float(self): """Reads a real floating point (4 bytes) value.""" return unpack('= 0) and (len(result) != length): raise BufferError( 'No more data left to read (need {}, got {}: {}); last read {}' .format(length, len(result), repr(result), repr(self._last)) ) self._last = result return result def get_bytes(self): """Gets the byte array representing the current buffer as a whole.""" return self.stream.getvalue() # endregion # region Telegram custom reading def tgread_bytes(self): """ Reads a Telegram-encoded byte array, without the need of specifying its length. """ first_byte = self.read_byte() if first_byte == 254: length = self.read_byte() | (self.read_byte() << 8) | ( self.read_byte() << 16) padding = length % 4 else: length = first_byte padding = (length + 1) % 4 data = self.read(length) if padding > 0: padding = 4 - padding self.read(padding) return data def tgread_string(self): """Reads a Telegram-encoded string.""" return str(self.tgread_bytes(), encoding='utf-8', errors='replace') def tgread_bool(self): """Reads a Telegram boolean value.""" value = self.read_int(signed=False) if value == 0x997275b5: # boolTrue return True elif value == 0xbc799737: # boolFalse return False else: raise RuntimeError('Invalid boolean code {}'.format(hex(value))) def tgread_date(self): """Reads and converts Unix time (used by Telegram) into a Python datetime object. """ value = self.read_int() return _EPOCH + timedelta(seconds=value) def tgread_object(self): """Reads a Telegram object.""" constructor_id = self.read_int(signed=False) clazz = tlobjects.get(constructor_id, None) if clazz is None: # The class was None, but there's still a # chance of it being a manually parsed value like bool! value = constructor_id if value == 0x997275b5: # boolTrue return True elif value == 0xbc799737: # boolFalse return False elif value == 0x1cb5c415: # Vector return [self.tgread_object() for _ in range(self.read_int())] clazz = core_objects.get(constructor_id, None) if clazz is None: # If there was still no luck, give up self.seek(-4) # Go back pos = self.tell_position() error = TypeNotFoundError(constructor_id, self.read()) self.set_position(pos) raise error return clazz.from_reader(self) def tgread_vector(self): """Reads a vector (a list) of Telegram objects.""" if 0x1cb5c415 != self.read_int(signed=False): raise RuntimeError('Invalid constructor code, vector was expected') count = self.read_int() return [self.tgread_object() for _ in range(count)] # endregion def close(self): """Closes the reader, freeing the BytesIO stream.""" self.stream.close() # region Position related def tell_position(self): """Tells the current position on the stream.""" return self.stream.tell() def set_position(self, position): """Sets the current position on the stream.""" self.stream.seek(position) def seek(self, offset): """ Seeks the stream position given an offset from the current position. The offset may be negative. """ self.stream.seek(offset, os.SEEK_CUR) # endregion # region with block def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() # endregion Telethon-1.39.0/telethon/extensions/html.py000066400000000000000000000150711475566265000207510ustar00rootroot00000000000000""" Simple HTML -> Telegram entity parser. """ from collections import deque from html import escape from html.parser import HTMLParser from typing import Iterable, Tuple, List from ..helpers import add_surrogate, del_surrogate, within_surrogate, strip_text from ..tl import TLObject from ..tl.types import ( MessageEntityBold, MessageEntityItalic, MessageEntityCode, MessageEntityPre, MessageEntityEmail, MessageEntityUrl, MessageEntityTextUrl, MessageEntityMentionName, MessageEntityUnderline, MessageEntityStrike, MessageEntityBlockquote, TypeMessageEntity ) class HTMLToTelegramParser(HTMLParser): def __init__(self): super().__init__() self.text = '' self.entities = [] self._building_entities = {} self._open_tags = deque() self._open_tags_meta = deque() def handle_starttag(self, tag, attrs): self._open_tags.appendleft(tag) self._open_tags_meta.appendleft(None) attrs = dict(attrs) EntityType = None args = {} if tag == 'strong' or tag == 'b': EntityType = MessageEntityBold elif tag == 'em' or tag == 'i': EntityType = MessageEntityItalic elif tag == 'u': EntityType = MessageEntityUnderline elif tag == 'del' or tag == 's': EntityType = MessageEntityStrike elif tag == 'blockquote': EntityType = MessageEntityBlockquote elif tag == 'code': try: # If we're in the middle of a
 tag, this  tag is
                # probably intended for syntax highlighting.
                #
                # Syntax highlighting is set with
                #     codeblock
                # inside 
 tags
                pre = self._building_entities['pre']
                try:
                    pre.language = attrs['class'][len('language-'):]
                except KeyError:
                    pass
            except KeyError:
                EntityType = MessageEntityCode
        elif tag == 'pre':
            EntityType = MessageEntityPre
            args['language'] = ''
        elif tag == 'a':
            try:
                url = attrs['href']
            except KeyError:
                return
            if url.startswith('mailto:'):
                url = url[len('mailto:'):]
                EntityType = MessageEntityEmail
            else:
                if self.get_starttag_text() == url:
                    EntityType = MessageEntityUrl
                else:
                    EntityType = MessageEntityTextUrl
                    args['url'] = del_surrogate(url)
                    url = None
            self._open_tags_meta.popleft()
            self._open_tags_meta.appendleft(url)

        if EntityType and tag not in self._building_entities:
            self._building_entities[tag] = EntityType(
                offset=len(self.text),
                # The length will be determined when closing the tag.
                length=0,
                **args)

    def handle_data(self, text):
        previous_tag = self._open_tags[0] if len(self._open_tags) > 0 else ''
        if previous_tag == 'a':
            url = self._open_tags_meta[0]
            if url:
                text = url

        for tag, entity in self._building_entities.items():
            entity.length += len(text)

        self.text += text

    def handle_endtag(self, tag):
        try:
            self._open_tags.popleft()
            self._open_tags_meta.popleft()
        except IndexError:
            pass
        entity = self._building_entities.pop(tag, None)
        if entity:
            self.entities.append(entity)


def parse(html: str) -> Tuple[str, List[TypeMessageEntity]]:
    """
    Parses the given HTML message and returns its stripped representation
    plus a list of the MessageEntity's that were found.

    :param html: the message with HTML to be parsed.
    :return: a tuple consisting of (clean message, [message entities]).
    """
    if not html:
        return html, []

    parser = HTMLToTelegramParser()
    parser.feed(add_surrogate(html))
    text = strip_text(parser.text, parser.entities)
    parser.entities.reverse()
    parser.entities.sort(key=lambda entity: entity.offset)
    return del_surrogate(text), parser.entities


ENTITY_TO_FORMATTER = {
    MessageEntityBold: ('', ''),
    MessageEntityItalic: ('', ''),
    MessageEntityCode: ('', ''),
    MessageEntityUnderline: ('', ''),
    MessageEntityStrike: ('', ''),
    MessageEntityBlockquote: ('
', '
'), MessageEntityPre: lambda e, _: ( "
\n"
        "    \n"
        "        ".format(e.language), "{}\n"
        "    \n"
        "
" ), MessageEntityEmail: lambda _, t: (''.format(t), ''), MessageEntityUrl: lambda _, t: (''.format(t), ''), MessageEntityTextUrl: lambda e, _: (''.format(escape(e.url)), ''), MessageEntityMentionName: lambda e, _: (''.format(e.user_id), ''), } def unparse(text: str, entities: Iterable[TypeMessageEntity]) -> str: """ Performs the reverse operation to .parse(), effectively returning HTML given a normal text and its MessageEntity's. :param text: the text to be reconverted into HTML. :param entities: the MessageEntity's applied to the text. :return: a HTML representation of the combination of both inputs. """ if not text: return text elif not entities: return escape(text) if isinstance(entities, TLObject): entities = (entities,) text = add_surrogate(text) insert_at = [] for i, entity in enumerate(entities): s = entity.offset e = entity.offset + entity.length delimiter = ENTITY_TO_FORMATTER.get(type(entity), None) if delimiter: if callable(delimiter): delimiter = delimiter(entity, text[s:e]) insert_at.append((s, i, delimiter[0])) insert_at.append((e, -i, delimiter[1])) insert_at.sort(key=lambda t: (t[0], t[1])) next_escape_bound = len(text) while insert_at: # Same logic as markdown.py at, _, what = insert_at.pop() while within_surrogate(text, at): at += 1 text = text[:at] + what + escape(text[at:next_escape_bound]) + text[next_escape_bound:] next_escape_bound = at text = escape(text[:next_escape_bound]) + text[next_escape_bound:] return del_surrogate(text) Telethon-1.39.0/telethon/extensions/markdown.py000066400000000000000000000153731475566265000216340ustar00rootroot00000000000000""" Simple markdown parser which does not support nesting. Intended primarily for use within the library, which attempts to handle emojies correctly, since they seem to count as two characters and it's a bit strange. """ import re import warnings from ..helpers import add_surrogate, del_surrogate, within_surrogate, strip_text from ..tl import TLObject from ..tl.types import ( MessageEntityBold, MessageEntityItalic, MessageEntityCode, MessageEntityPre, MessageEntityTextUrl, MessageEntityMentionName, MessageEntityStrike ) DEFAULT_DELIMITERS = { '**': MessageEntityBold, '__': MessageEntityItalic, '~~': MessageEntityStrike, '`': MessageEntityCode, '```': MessageEntityPre } DEFAULT_URL_RE = re.compile(r'\[([\s\S]+?)\]\((.+?)\)') DEFAULT_URL_FORMAT = '[{0}]({1})' def parse(message, delimiters=None, url_re=None): """ Parses the given markdown message and returns its stripped representation plus a list of the MessageEntity's that were found. :param message: the message with markdown-like syntax to be parsed. :param delimiters: the delimiters to be used, {delimiter: type}. :param url_re: the URL bytes regex to be used. Must have two groups. :return: a tuple consisting of (clean message, [message entities]). """ if not message: return message, [] if url_re is None: url_re = DEFAULT_URL_RE elif isinstance(url_re, str): url_re = re.compile(url_re) if not delimiters: if delimiters is not None: return message, [] delimiters = DEFAULT_DELIMITERS # Build a regex to efficiently test all delimiters at once. # Note that the largest delimiter should go first, we don't # want ``` to be interpreted as a single back-tick in a code block. delim_re = re.compile('|'.join('({})'.format(re.escape(k)) for k in sorted(delimiters, key=len, reverse=True))) # Cannot use a for loop because we need to skip some indices i = 0 result = [] # Work on byte level with the utf-16le encoding to get the offsets right. # The offset will just be half the index we're at. message = add_surrogate(message) while i < len(message): m = delim_re.match(message, pos=i) # Did we find some delimiter here at `i`? if m: delim = next(filter(None, m.groups())) # +1 to avoid matching right after (e.g. "****") end = message.find(delim, i + len(delim) + 1) # Did we find the earliest closing tag? if end != -1: # Remove the delimiter from the string message = ''.join(( message[:i], message[i + len(delim):end], message[end + len(delim):] )) # Check other affected entities for ent in result: # If the end is after our start, it is affected if ent.offset + ent.length > i: # If the old start is before ours and the old end is after ours, we are fully enclosed if ent.offset <= i and ent.offset + ent.length >= end + len(delim): ent.length -= len(delim) * 2 else: ent.length -= len(delim) # Append the found entity ent = delimiters[delim] if ent == MessageEntityPre: result.append(ent(i, end - i - len(delim), '')) # has 'lang' else: result.append(ent(i, end - i - len(delim))) # No nested entities inside code blocks if ent in (MessageEntityCode, MessageEntityPre): i = end - len(delim) continue elif url_re: m = url_re.match(message, pos=i) if m: # Replace the whole match with only the inline URL text. message = ''.join(( message[:m.start()], m.group(1), message[m.end():] )) delim_size = m.end() - m.start() - len(m.group(1)) for ent in result: # If the end is after our start, it is affected if ent.offset + ent.length > m.start(): ent.length -= delim_size result.append(MessageEntityTextUrl( offset=m.start(), length=len(m.group(1)), url=del_surrogate(m.group(2)) )) i += len(m.group(1)) continue i += 1 message = strip_text(message, result) return del_surrogate(message), result def unparse(text, entities, delimiters=None, url_fmt=None): """ Performs the reverse operation to .parse(), effectively returning markdown-like syntax given a normal text and its MessageEntity's. :param text: the text to be reconverted into markdown. :param entities: the MessageEntity's applied to the text. :return: a markdown-like text representing the combination of both inputs. """ if not text or not entities: return text if not delimiters: if delimiters is not None: return text delimiters = DEFAULT_DELIMITERS if url_fmt is not None: warnings.warn('url_fmt is deprecated') # since it complicates everything *a lot* if isinstance(entities, TLObject): entities = (entities,) text = add_surrogate(text) delimiters = {v: k for k, v in delimiters.items()} insert_at = [] for i, entity in enumerate(entities): s = entity.offset e = entity.offset + entity.length delimiter = delimiters.get(type(entity), None) if delimiter: insert_at.append((s, i, delimiter)) insert_at.append((e, -i, delimiter)) else: url = None if isinstance(entity, MessageEntityTextUrl): url = entity.url elif isinstance(entity, MessageEntityMentionName): url = 'tg://user?id={}'.format(entity.user_id) if url: insert_at.append((s, i, '[')) insert_at.append((e, -i, ']({})'.format(url))) insert_at.sort(key=lambda t: (t[0], t[1])) while insert_at: at, _, what = insert_at.pop() # If we are in the middle of a surrogate nudge the position by -1. # Otherwise we would end up with malformed text and fail to encode. # For example of bad input: "Hi \ud83d\ude1c" # https://en.wikipedia.org/wiki/UTF-16#U+010000_to_U+10FFFF while within_surrogate(text, at): at += 1 text = text[:at] + what + text[at:] return del_surrogate(text) Telethon-1.39.0/telethon/extensions/messagepacker.py000066400000000000000000000077531475566265000226270ustar00rootroot00000000000000import asyncio import collections import io import struct from ..tl import TLRequest from ..tl.core.messagecontainer import MessageContainer from ..tl.core.tlmessage import TLMessage class MessagePacker: """ This class packs `RequestState` as outgoing `TLMessages`. The purpose of this class is to support putting N `RequestState` into a queue, and then awaiting for "packed" `TLMessage` in the other end. The simplest case would be ``State -> TLMessage`` (1-to-1 relationship) but for efficiency purposes it's ``States -> Container`` (N-to-1). This addresses several needs: outgoing messages will be smaller, so the encryption and network overhead also is smaller. It's also a central point where outgoing requests are put, and where ready-messages are get. """ def __init__(self, state, loggers): self._state = state self._deque = collections.deque() self._ready = asyncio.Event() self._log = loggers[__name__] def append(self, state): self._deque.append(state) self._ready.set() def extend(self, states): self._deque.extend(states) self._ready.set() async def get(self): """ Returns (batch, data) if one or more items could be retrieved. If the cancellation occurs or only invalid items were in the queue, (None, None) will be returned instead. """ if not self._deque: self._ready.clear() await self._ready.wait() buffer = io.BytesIO() batch = [] size = 0 # Fill a new batch to return while the size is small enough, # as long as we don't exceed the maximum length of messages. while self._deque and len(batch) <= MessageContainer.MAXIMUM_LENGTH: state = self._deque.popleft() size += len(state.data) + TLMessage.SIZE_OVERHEAD if size <= MessageContainer.MAXIMUM_SIZE: state.msg_id = self._state.write_data_as_message( buffer, state.data, isinstance(state.request, TLRequest), after_id=state.after.msg_id if state.after else None ) batch.append(state) self._log.debug('Assigned msg_id = %d to %s (%x)', state.msg_id, state.request.__class__.__name__, id(state.request)) continue if batch: # Put the item back since it can't be sent in this batch self._deque.appendleft(state) break # If a single message exceeds the maximum size, then the # message payload cannot be sent. Telegram would forcibly # close the connection; message would never be confirmed. # # We don't put the item back because it can never be sent. # If we did, we would loop again and reach this same path. # Setting the exception twice results in `InvalidStateError` # and this method should never return with error, which we # really want to avoid. self._log.warning( 'Message payload for %s is too long (%d) and cannot be sent', state.request.__class__.__name__, len(state.data) ) state.future.set_exception( ValueError('Request payload is too big')) size = 0 continue if not batch: return None, None if len(batch) > 1: # Inlined code to pack several messages into a container data = struct.pack( ' Surrogate Pairs (Telegram offsets are calculated with these). # See https://en.wikipedia.org/wiki/Plane_(Unicode)#Overview for more. ''.join(chr(y) for y in struct.unpack(' left_offset: if e.offset >= left_offset: # 0 1|2 3 4 5 | 0 1|2 3 4 5 # ^ ^ | ^ # lo(2) o(5) | o(2)/lo(2) e.offset -= left_offset # |0 1 2 3 | |0 1 2 3 # ^ | ^ # o=o-lo(3=5-2) | o=o-lo(0=2-2) else: # e.offset < left_offset and e.offset + e.length > left_offset # 0 1 2 3|4 5 6 7 8 9 10 # ^ ^ ^ # o(1) lo(4) o+l(1+9) e.length = e.offset + e.length - left_offset e.offset = 0 # |0 1 2 3 4 5 6 # ^ ^ # o(0) o+l=0+o+l-lo(6=0+6=0+1+9-4) else: # e.offset + e.length <= left_offset # 0 1 2 3|4 5 # ^ ^ # o(0) o+l(4) # lo(4) del entities[i] continue if e.offset + e.length <= len_final: # |0 1 2 3 4 5 6 7 8 9 # ^ ^ # o(1) o+l(1+9)/lf(10) continue if e.offset >= len_final: # |0 1 2 3 4 # ^ # o(5)/lf(5) del entities[i] else: # e.offset < len_final and e.offset + e.length > len_final # |0 1 2 3 4 5 (6) (7) (8) (9) # ^ ^ ^ # o(1) lf(6) o+l(1+8) e.length = len_final - e.offset # |0 1 2 3 4 5 # ^ ^ # o(1) o+l=o+lf-o=lf(6=1+5=1+6-1) return text def retry_range(retries, force_retry=True): """ Generates an integer sequence starting from 1. If `retries` is not a zero or a positive integer value, the sequence will be infinite, otherwise it will end at `retries + 1`. """ # We need at least one iteration even if the retries are 0 # when force_retry is True. if force_retry and not (retries is None or retries < 0): retries += 1 attempt = 0 while attempt != retries: attempt += 1 yield attempt async def _maybe_await(value): if inspect.isawaitable(value): return await value else: return value async def _cancel(log, **tasks): """ Helper to cancel one or more tasks gracefully, logging exceptions. """ for name, task in tasks.items(): if not task: continue task.cancel() try: await task except asyncio.CancelledError: pass except RuntimeError: # Probably: RuntimeError: await wasn't used with future # # See: https://github.com/python/cpython/blob/12d3061c7819a73d891dcce44327410eaf0e1bc2/Lib/asyncio/futures.py#L265 # # Happens with _asyncio.Task instances (in "Task cancelling" state) # trying to SIGINT the program right during initial connection, on # _recv_loop coroutine (but we're creating its task explicitly with # a loop, so how can it bug out like this?). # # Since we're aware of this error there's no point in logging it. # *May* be https://bugs.python.org/issue37172 pass except AssertionError as e: # In Python 3.6, the above RuntimeError is an AssertionError # See https://github.com/python/cpython/blob/7df32f844efed33ca781a016017eab7050263b90/Lib/asyncio/futures.py#L328 if e.args != ("yield from wasn't used with future",): log.exception('Unhandled exception from %s after cancelling ' '%s (%s)', name, type(task), task) except Exception: log.exception('Unhandled exception from %s after cancelling ' '%s (%s)', name, type(task), task) def _sync_enter(self): """ Helps to cut boilerplate on async context managers that offer synchronous variants. """ if hasattr(self, 'loop'): loop = self.loop else: loop = self._client.loop if loop.is_running(): raise RuntimeError( 'You must use "async with" if the event loop ' 'is running (i.e. you are inside an "async def")' ) return loop.run_until_complete(self.__aenter__()) def _sync_exit(self, *args): if hasattr(self, 'loop'): loop = self.loop else: loop = self._client.loop return loop.run_until_complete(self.__aexit__(*args)) def _entity_type(entity): # This could be a `utils` method that just ran a few `isinstance` on # `utils.get_peer(...)`'s result. However, there are *a lot* of auto # casts going on, plenty of calls and temporary short-lived objects. # # So we just check if a string is in the class name. # Still, assert that it's the right type to not return false results. try: if entity.SUBCLASS_OF_ID not in ( 0x2d45687, # crc32(b'Peer') 0xc91c90b6, # crc32(b'InputPeer') 0xe669bf46, # crc32(b'InputUser') 0x40f202fd, # crc32(b'InputChannel') 0x2da17977, # crc32(b'User') 0xc5af5d94, # crc32(b'Chat') 0x1f4661b9, # crc32(b'UserFull') 0xd49a2697, # crc32(b'ChatFull') ): raise TypeError('{} does not have any entity type'.format(entity)) except AttributeError: raise TypeError('{} is not a TLObject, cannot determine entity type'.format(entity)) name = entity.__class__.__name__ if 'User' in name: return _EntityType.USER elif 'Chat' in name: return _EntityType.CHAT elif 'Channel' in name: return _EntityType.CHANNEL elif 'Self' in name: return _EntityType.USER # 'Empty' in name or not found, we don't care, not a valid entity. raise TypeError('{} does not have any entity type'.format(entity)) # endregion # region Cryptographic related utils def generate_key_data_from_nonce(server_nonce, new_nonce): """Generates the key data corresponding to the given nonce""" server_nonce = server_nonce.to_bytes(16, 'little', signed=True) new_nonce = new_nonce.to_bytes(32, 'little', signed=True) hash1 = sha1(new_nonce + server_nonce).digest() hash2 = sha1(server_nonce + new_nonce).digest() hash3 = sha1(new_nonce + new_nonce).digest() key = hash1 + hash2[:12] iv = hash2[12:20] + hash3 + new_nonce[:4] return key, iv # endregion # region Custom Classes class TotalList(list): """ A list with an extra `total` property, which may not match its `len` since the total represents the total amount of items *available* somewhere else, not the items *in this list*. Examples: .. code-block:: python # Telethon returns these lists in some cases (for example, # only when a chunk is returned, but the "total" count # is available). result = await client.get_messages(chat, limit=10) print(result.total) # large number print(len(result)) # 10 print(result[0]) # latest message for x in result: # show the 10 messages print(x.text) """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.total = 0 def __str__(self): return '[{}, total={}]'.format( ', '.join(str(x) for x in self), self.total) def __repr__(self): return '[{}, total={}]'.format( ', '.join(repr(x) for x in self), self.total) class _FileStream(io.IOBase): """ Proxy around things that represent a file and need to be used as streams which may or not need to be closed. This will handle `pathlib.Path`, `str` paths, in-memory `bytes`, and anything IO-like (including `aiofiles`). It also provides access to the name and file size (also necessary). """ def __init__(self, file, *, file_size=None): if isinstance(file, Path): file = str(file.absolute()) self._file = file self._name = None self._size = file_size self._stream = None self._close_stream = None async def __aenter__(self): if isinstance(self._file, str): self._name = os.path.basename(self._file) self._size = os.path.getsize(self._file) self._stream = open(self._file, 'rb') self._close_stream = True return self if isinstance(self._file, bytes): self._size = len(self._file) self._stream = io.BytesIO(self._file) self._close_stream = True return self if not callable(getattr(self._file, 'read', None)): raise TypeError('file description should have a `read` method') self._name = getattr(self._file, 'name', None) self._stream = self._file self._close_stream = False if self._size is None: if callable(getattr(self._file, 'seekable', None)): seekable = await _maybe_await(self._file.seekable()) else: seekable = False if seekable: pos = await _maybe_await(self._file.tell()) await _maybe_await(self._file.seek(0, os.SEEK_END)) self._size = await _maybe_await(self._file.tell()) await _maybe_await(self._file.seek(pos, os.SEEK_SET)) else: _log.warning( 'Could not determine file size beforehand so the entire ' 'file will be read in-memory') data = await _maybe_await(self._file.read()) self._size = len(data) self._stream = io.BytesIO(data) self._close_stream = True return self async def __aexit__(self, exc_type, exc_val, exc_tb): if self._close_stream and self._stream: await _maybe_await(self._stream.close()) @property def file_size(self): return self._size @property def name(self): return self._name # Proxy all the methods. Doesn't need to be readable (makes multiline edits easier) def read(self, *args, **kwargs): return self._stream.read(*args, **kwargs) def readinto(self, *args, **kwargs): return self._stream.readinto(*args, **kwargs) def write(self, *args, **kwargs): return self._stream.write(*args, **kwargs) def fileno(self, *args, **kwargs): return self._stream.fileno(*args, **kwargs) def flush(self, *args, **kwargs): return self._stream.flush(*args, **kwargs) def isatty(self, *args, **kwargs): return self._stream.isatty(*args, **kwargs) def readable(self, *args, **kwargs): return self._stream.readable(*args, **kwargs) def readline(self, *args, **kwargs): return self._stream.readline(*args, **kwargs) def readlines(self, *args, **kwargs): return self._stream.readlines(*args, **kwargs) def seek(self, *args, **kwargs): return self._stream.seek(*args, **kwargs) def seekable(self, *args, **kwargs): return self._stream.seekable(*args, **kwargs) def tell(self, *args, **kwargs): return self._stream.tell(*args, **kwargs) def truncate(self, *args, **kwargs): return self._stream.truncate(*args, **kwargs) def writable(self, *args, **kwargs): return self._stream.writable(*args, **kwargs) def writelines(self, *args, **kwargs): return self._stream.writelines(*args, **kwargs) # close is special because it will be called by __del__ but we do NOT # want to close the file unless we have to (we're just a wrapper). # Instead, we do nothing (we should be used through the decorator which # has its own mechanism to close the file correctly). def close(self, *args, **kwargs): pass # endregion def get_running_loop(): if sys.version_info >= (3, 7): try: return asyncio.get_running_loop() except RuntimeError: return asyncio.get_event_loop_policy().get_event_loop() else: return asyncio.get_event_loop() Telethon-1.39.0/telethon/hints.py000066400000000000000000000030321475566265000167250ustar00rootroot00000000000000import datetime import typing from . import helpers from .tl import types, custom Phone = str Username = str PeerID = int Entity = typing.Union[types.User, types.Chat, types.Channel] FullEntity = typing.Union[types.UserFull, types.messages.ChatFull, types.ChatFull, types.ChannelFull] EntityLike = typing.Union[ Phone, Username, PeerID, types.TypePeer, types.TypeInputPeer, Entity, FullEntity ] EntitiesLike = typing.Union[EntityLike, typing.Sequence[EntityLike]] ButtonLike = typing.Union[types.TypeKeyboardButton, custom.Button] MarkupLike = typing.Union[ types.TypeReplyMarkup, ButtonLike, typing.Sequence[ButtonLike], typing.Sequence[typing.Sequence[ButtonLike]] ] TotalList = helpers.TotalList DateLike = typing.Optional[typing.Union[float, datetime.datetime, datetime.date, datetime.timedelta]] LocalPath = str ExternalUrl = str BotFileID = str FileLike = typing.Union[ LocalPath, ExternalUrl, BotFileID, bytes, typing.BinaryIO, types.TypeMessageMedia, types.TypeInputFile, types.TypeInputFileLocation ] # Can't use `typing.Type` in Python 3.5.2 # See https://github.com/python/typing/issues/266 try: OutFileLike = typing.Union[ str, typing.Type[bytes], typing.BinaryIO ] except TypeError: OutFileLike = typing.Union[ str, typing.BinaryIO ] MessageLike = typing.Union[str, types.Message] MessageIDLike = typing.Union[int, types.Message, types.TypeInputMessage] ProgressCallback = typing.Callable[[int, int], None] Telethon-1.39.0/telethon/network/000077500000000000000000000000001475566265000167215ustar00rootroot00000000000000Telethon-1.39.0/telethon/network/__init__.py000066400000000000000000000011111475566265000210240ustar00rootroot00000000000000""" This module contains several classes regarding network, low level connection with Telegram's servers and the protocol used (TCP full, abridged, etc.). """ from .mtprotoplainsender import MTProtoPlainSender from .authenticator import do_authentication from .mtprotosender import MTProtoSender from .connection import ( Connection, ConnectionTcpFull, ConnectionTcpIntermediate, ConnectionTcpAbridged, ConnectionTcpObfuscated, ConnectionTcpMTProxyAbridged, ConnectionTcpMTProxyIntermediate, ConnectionTcpMTProxyRandomizedIntermediate, ConnectionHttp, TcpMTProxy ) Telethon-1.39.0/telethon/network/authenticator.py000066400000000000000000000172751475566265000221610ustar00rootroot00000000000000""" 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 from ..tl.types import ( ResPQ, PQInnerData, ServerDHParamsFail, ServerDHParamsOk, ServerDHInnerData, ClientDHInnerData, DhGenOk, DhGenRetry, DhGenFail ) from .. import helpers from ..crypto import AES, AuthKey, Factorization, rsa from ..errors import SecurityError from ..extensions import BinaryReader from ..tl.functions import ( ReqPqMultiRequest, ReqDHParamsRequest, SetClientDHParamsRequest ) async def do_authentication(sender): """ Executes the authentication process with the Telegram servers. :param sender: a connected `MTProtoPlainSender`. :return: returns a (authorization key, time offset) tuple. """ # Step 1 sending: PQ Request, endianness doesn't matter since it's random nonce = int.from_bytes(os.urandom(16), 'big', signed=True) res_pq = await sender.send(ReqPqMultiRequest(nonce)) assert isinstance(res_pq, ResPQ), 'Step 1 answer was %s' % res_pq if res_pq.nonce != nonce: raise SecurityError('Step 1 invalid nonce from server') pq = get_int(res_pq.pq) # Step 2 sending: DH Exchange p, q = Factorization.factorize(pq) p, q = rsa.get_byte_array(p), rsa.get_byte_array(q) new_nonce = int.from_bytes(os.urandom(32), 'little', signed=True) pq_inner_data = bytes(PQInnerData( pq=rsa.get_byte_array(pq), p=p, q=q, nonce=res_pq.nonce, server_nonce=res_pq.server_nonce, new_nonce=new_nonce )) # sha_digest + data + random_bytes cipher_text, target_fingerprint = None, None for fingerprint in res_pq.server_public_key_fingerprints: cipher_text = rsa.encrypt(fingerprint, pq_inner_data) if cipher_text is not None: target_fingerprint = fingerprint break if cipher_text is None: # Second attempt, but now we're allowed to use old keys for fingerprint in res_pq.server_public_key_fingerprints: cipher_text = rsa.encrypt(fingerprint, pq_inner_data, use_old=True) if cipher_text is not None: target_fingerprint = fingerprint break if cipher_text is None: raise SecurityError( 'Step 2 could not find a valid key for fingerprints: {}' .format(', '.join( [str(f) for f in res_pq.server_public_key_fingerprints]) ) ) server_dh_params = await sender.send(ReqDHParamsRequest( nonce=res_pq.nonce, server_nonce=res_pq.server_nonce, p=p, q=q, public_key_fingerprint=target_fingerprint, encrypted_data=cipher_text )) assert isinstance( server_dh_params, (ServerDHParamsOk, ServerDHParamsFail)),\ 'Step 2.1 answer was %s' % server_dh_params if server_dh_params.nonce != res_pq.nonce: raise SecurityError('Step 2 invalid nonce from server') if server_dh_params.server_nonce != res_pq.server_nonce: raise SecurityError('Step 2 invalid server nonce from server') if isinstance(server_dh_params, ServerDHParamsFail): nnh = int.from_bytes( sha1(new_nonce.to_bytes(32, 'little', signed=True)).digest()[4:20], 'little', signed=True ) if server_dh_params.new_nonce_hash != nnh: raise SecurityError('Step 2 invalid DH fail nonce from server') assert isinstance(server_dh_params, ServerDHParamsOk),\ 'Step 2.2 answer was %s' % server_dh_params # Step 3 sending: Complete DH Exchange key, iv = helpers.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('Step 3 AES block size mismatch') plain_text_answer = AES.decrypt_ige( server_dh_params.encrypted_answer, key, iv ) with BinaryReader(plain_text_answer) as reader: reader.read(20) # hash sum server_dh_inner = reader.tgread_object() assert isinstance(server_dh_inner, ServerDHInnerData),\ 'Step 3 answer was %s' % server_dh_inner if server_dh_inner.nonce != res_pq.nonce: raise SecurityError('Step 3 Invalid nonce in encrypted answer') if server_dh_inner.server_nonce != res_pq.server_nonce: raise SecurityError('Step 3 Invalid server nonce in encrypted answer') dh_prime = get_int(server_dh_inner.dh_prime, signed=False) g = server_dh_inner.g g_a = get_int(server_dh_inner.g_a, signed=False) time_offset = server_dh_inner.server_time - int(time.time()) b = get_int(os.urandom(256), signed=False) g_b = pow(g, b, dh_prime) gab = pow(g_a, b, dh_prime) # IMPORTANT: Apart from the conditions on the Diffie-Hellman prime # dh_prime and generator g, both sides are to check that g, g_a and # g_b are greater than 1 and less than dh_prime - 1. We recommend # checking that g_a and g_b are between 2^{2048-64} and # dh_prime - 2^{2048-64} as well. # (https://core.telegram.org/mtproto/auth_key#dh-key-exchange-complete) if not (1 < g < (dh_prime - 1)): raise SecurityError('g_a is not within (1, dh_prime - 1)') if not (1 < g_a < (dh_prime - 1)): raise SecurityError('g_a is not within (1, dh_prime - 1)') if not (1 < g_b < (dh_prime - 1)): raise SecurityError('g_b is not within (1, dh_prime - 1)') safety_range = 2 ** (2048 - 64) if not (safety_range <= g_a <= (dh_prime - safety_range)): raise SecurityError('g_a is not within (2^{2048-64}, dh_prime - 2^{2048-64})') if not (safety_range <= g_b <= (dh_prime - safety_range)): raise SecurityError('g_b is not within (2^{2048-64}, dh_prime - 2^{2048-64})') # Prepare client DH Inner Data client_dh_inner = bytes(ClientDHInnerData( nonce=res_pq.nonce, server_nonce=res_pq.server_nonce, retry_id=0, # TODO Actual retry ID g_b=rsa.get_byte_array(g_b) )) client_dh_inner_hashed = sha1(client_dh_inner).digest() + client_dh_inner # Encryption client_dh_encrypted = AES.encrypt_ige(client_dh_inner_hashed, key, iv) # Prepare Set client DH params dh_gen = await sender.send(SetClientDHParamsRequest( nonce=res_pq.nonce, server_nonce=res_pq.server_nonce, encrypted_data=client_dh_encrypted, )) nonce_types = (DhGenOk, DhGenRetry, DhGenFail) assert isinstance(dh_gen, nonce_types), 'Step 3.1 answer was %s' % dh_gen name = dh_gen.__class__.__name__ if dh_gen.nonce != res_pq.nonce: raise SecurityError('Step 3 invalid {} nonce from server'.format(name)) if dh_gen.server_nonce != res_pq.server_nonce: raise SecurityError( 'Step 3 invalid {} server nonce from server'.format(name)) auth_key = AuthKey(rsa.get_byte_array(gab)) nonce_number = 1 + nonce_types.index(type(dh_gen)) new_nonce_hash = auth_key.calc_new_nonce_hash(new_nonce, nonce_number) dh_hash = getattr(dh_gen, 'new_nonce_hash{}'.format(nonce_number)) if dh_hash != new_nonce_hash: raise SecurityError('Step 3 invalid new nonce hash') if not isinstance(dh_gen, DhGenOk): raise AssertionError('Step 3.2 answer was %s' % dh_gen) return auth_key, time_offset def get_int(byte_array, signed=True): """ 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) Telethon-1.39.0/telethon/network/connection/000077500000000000000000000000001475566265000210605ustar00rootroot00000000000000Telethon-1.39.0/telethon/network/connection/__init__.py000066400000000000000000000006471475566265000232000ustar00rootroot00000000000000from .connection import Connection from .tcpfull import ConnectionTcpFull from .tcpintermediate import ConnectionTcpIntermediate from .tcpabridged import ConnectionTcpAbridged from .tcpobfuscated import ConnectionTcpObfuscated from .tcpmtproxy import ( TcpMTProxy, ConnectionTcpMTProxyAbridged, ConnectionTcpMTProxyIntermediate, ConnectionTcpMTProxyRandomizedIntermediate ) from .http import ConnectionHttp Telethon-1.39.0/telethon/network/connection/connection.py000066400000000000000000000374411475566265000236020ustar00rootroot00000000000000import abc import asyncio import socket import sys try: import ssl as ssl_mod except ImportError: ssl_mod = None try: import python_socks except ImportError: python_socks = None from ...errors import InvalidChecksumError, InvalidBufferError from ... import helpers class Connection(abc.ABC): """ The `Connection` class is a wrapper around ``asyncio.open_connection``. Subclasses will implement different transport modes as atomic operations, which this class eases doing since the exposed interface simply puts and gets complete data payloads to and from queues. The only error that will raise from send and receive methods is ``ConnectionError``, which will raise when attempting to send if the client is disconnected (includes remote disconnections). """ # this static attribute should be redefined by `Connection` subclasses and # should be one of `PacketCodec` implementations packet_codec = None def __init__(self, ip, port, dc_id, *, loggers, proxy=None, local_addr=None): self._ip = ip self._port = port self._dc_id = dc_id # only for MTProxy, it's an abstraction leak self._log = loggers[__name__] self._proxy = proxy self._local_addr = local_addr self._reader = None self._writer = None self._connected = False self._send_task = None self._recv_task = None self._codec = None self._obfuscation = None # TcpObfuscated and MTProxy self._send_queue = asyncio.Queue(1) self._recv_queue = asyncio.Queue(1) @staticmethod def _wrap_socket_ssl(sock): if ssl_mod is None: raise RuntimeError( 'Cannot use proxy that requires SSL ' 'without the SSL module being available' ) return ssl_mod.wrap_socket( sock, do_handshake_on_connect=True, ssl_version=ssl_mod.PROTOCOL_SSLv23, ciphers='ADH-AES256-SHA') @staticmethod def _parse_proxy(proxy_type, addr, port, rdns=True, username=None, password=None): if isinstance(proxy_type, str): proxy_type = proxy_type.lower() # Always prefer `python_socks` when available if python_socks: from python_socks import ProxyType # We do the check for numerical values here # to be backwards compatible with PySocks proxy format, # (since socks.SOCKS5 == 2, socks.SOCKS4 == 1, socks.HTTP == 3) if proxy_type == ProxyType.SOCKS5 or proxy_type == 2 or proxy_type == "socks5": protocol = ProxyType.SOCKS5 elif proxy_type == ProxyType.SOCKS4 or proxy_type == 1 or proxy_type == "socks4": protocol = ProxyType.SOCKS4 elif proxy_type == ProxyType.HTTP or proxy_type == 3 or proxy_type == "http": protocol = ProxyType.HTTP else: raise ValueError("Unknown proxy protocol type: {}".format(proxy_type)) # This tuple must be compatible with `python_socks`' `Proxy.create()` signature return protocol, addr, port, username, password, rdns else: from socks import SOCKS5, SOCKS4, HTTP if proxy_type == 2 or proxy_type == "socks5": protocol = SOCKS5 elif proxy_type == 1 or proxy_type == "socks4": protocol = SOCKS4 elif proxy_type == 3 or proxy_type == "http": protocol = HTTP else: raise ValueError("Unknown proxy protocol type: {}".format(proxy_type)) # This tuple must be compatible with `PySocks`' `socksocket.set_proxy()` signature return protocol, addr, port, rdns, username, password async def _proxy_connect(self, timeout=None, local_addr=None): if isinstance(self._proxy, (tuple, list)): parsed = self._parse_proxy(*self._proxy) elif isinstance(self._proxy, dict): parsed = self._parse_proxy(**self._proxy) else: raise TypeError("Proxy of unknown format: {}".format(type(self._proxy))) # Always prefer `python_socks` when available if python_socks: # python_socks internal errors are not inherited from # builtin IOError (just from Exception). Instead of adding those # in exceptions clauses everywhere through the code, we # rather monkey-patch them in place. Keep in mind that # ProxyError takes error_code as keyword argument. class ConnectionErrorExtra(ConnectionError): def __init__(self, message, error_code=None): super().__init__(message) self.error_code = error_code python_socks._errors.ProxyError = ConnectionErrorExtra python_socks._errors.ProxyConnectionError = ConnectionError python_socks._errors.ProxyTimeoutError = ConnectionError from python_socks.async_.asyncio import Proxy proxy = Proxy.create(*parsed) # WARNING: If `local_addr` is set we use manual socket creation, because, # unfortunately, `Proxy.connect()` does not expose `local_addr` # argument, so if we want to bind socket locally, we need to manually # create, bind and connect socket, and then pass to `Proxy.connect()` method. if local_addr is None: sock = await proxy.connect( dest_host=self._ip, dest_port=self._port, timeout=timeout ) else: # Here we start manual setup of the socket. # The `address` represents the proxy ip and proxy port, # not the destination one (!), because the socket # connects to the proxy server, not destination server. # IPv family is also checked on proxy address. if ':' in proxy.proxy_host: mode, address = socket.AF_INET6, (proxy.proxy_host, proxy.proxy_port, 0, 0) else: mode, address = socket.AF_INET, (proxy.proxy_host, proxy.proxy_port) # Create a non-blocking socket and bind it (if local address is specified). sock = socket.socket(mode, socket.SOCK_STREAM) sock.setblocking(False) sock.bind(local_addr) # Actual TCP connection is performed here. await asyncio.wait_for( helpers.get_running_loop().sock_connect(sock=sock, address=address), timeout=timeout ) # As our socket is already created and connected, # this call sets the destination host/port and # starts protocol negotiations with the proxy server. sock = await proxy.connect( dest_host=self._ip, dest_port=self._port, timeout=timeout, _socket=sock ) else: import socks # Here `address` represents destination address (not proxy), because of # the `PySocks` implementation of the connection routine. # IPv family is checked on proxy address, not destination address. if ':' in parsed[1]: mode, address = socket.AF_INET6, (self._ip, self._port, 0, 0) else: mode, address = socket.AF_INET, (self._ip, self._port) # Setup socket, proxy, timeout and bind it (if necessary). sock = socks.socksocket(mode, socket.SOCK_STREAM) sock.set_proxy(*parsed) sock.settimeout(timeout) if local_addr is not None: sock.bind(local_addr) # Actual TCP connection and negotiation performed here. await asyncio.wait_for( helpers.get_running_loop().sock_connect(sock=sock, address=address), timeout=timeout ) sock.setblocking(False) return sock async def _connect(self, timeout=None, ssl=None): if self._local_addr is not None: # NOTE: If port is not specified, we use 0 port # to notify the OS that port should be chosen randomly # from the available ones. if isinstance(self._local_addr, tuple) and len(self._local_addr) == 2: local_addr = self._local_addr elif isinstance(self._local_addr, str): local_addr = (self._local_addr, 0) else: raise ValueError("Unknown local address format: {}".format(self._local_addr)) else: local_addr = None if not self._proxy: self._reader, self._writer = await asyncio.wait_for( asyncio.open_connection( host=self._ip, port=self._port, ssl=ssl, local_addr=local_addr ), timeout=timeout) else: # Proxy setup, connection and negotiation is performed here. sock = await self._proxy_connect( timeout=timeout, local_addr=local_addr ) # Wrap socket in SSL context (if provided) if ssl: sock = self._wrap_socket_ssl(sock) self._reader, self._writer = await asyncio.open_connection(sock=sock) self._codec = self.packet_codec(self) self._init_conn() await self._writer.drain() async def connect(self, timeout=None, ssl=None): """ Establishes a connection with the server. """ await self._connect(timeout=timeout, ssl=ssl) self._connected = True loop = helpers.get_running_loop() self._send_task = loop.create_task(self._send_loop()) self._recv_task = loop.create_task(self._recv_loop()) async def disconnect(self): """ Disconnects from the server, and clears pending outgoing and incoming messages. """ if not self._connected: return self._connected = False await helpers._cancel( self._log, send_task=self._send_task, recv_task=self._recv_task ) if self._writer: self._writer.close() if sys.version_info >= (3, 7): try: await asyncio.wait_for(self._writer.wait_closed(), timeout=10) except asyncio.TimeoutError: # See issue #3917. For some users, this line was hanging indefinitely. # The hard timeout is not ideal (connection won't be properly closed), # but the code will at least be able to procceed. self._log.warning('Graceful disconnection timed out, forcibly ignoring cleanup') except Exception as e: # Disconnecting should never raise. Seen: # * OSError: No route to host and # * OSError: [Errno 32] Broken pipe # * ConnectionResetError self._log.info('%s during disconnect: %s', type(e), e) def send(self, data): """ Sends a packet of data through this connection mode. This method returns a coroutine. """ if not self._connected: raise ConnectionError('Not connected') return self._send_queue.put(data) async def recv(self): """ Receives a packet of data through this connection mode. This method returns a coroutine. """ while self._connected: result, err = await self._recv_queue.get() if err: raise err if result: return result raise ConnectionError('Not connected') async def _send_loop(self): """ This loop is constantly popping items off the queue to send them. """ try: while self._connected: self._send(await self._send_queue.get()) await self._writer.drain() except asyncio.CancelledError: pass except Exception as e: if isinstance(e, IOError): self._log.info('The server closed the connection while sending') else: self._log.exception('Unexpected exception in the send loop') await self.disconnect() async def _recv_loop(self): """ This loop is constantly putting items on the queue as they're read. """ try: while self._connected: try: data = await self._recv() except asyncio.CancelledError: break except (IOError, asyncio.IncompleteReadError) as e: self._log.warning('Server closed the connection: %s', e) await self._recv_queue.put((None, e)) await self.disconnect() except InvalidChecksumError as e: self._log.warning('Server response had invalid checksum: %s', e) await self._recv_queue.put((None, e)) except InvalidBufferError as e: self._log.warning('Server response had invalid buffer: %s', e) await self._recv_queue.put((None, e)) except Exception as e: self._log.exception('Unexpected exception in the receive loop') await self._recv_queue.put((None, e)) await self.disconnect() else: await self._recv_queue.put((data, None)) finally: await self.disconnect() def _init_conn(self): """ This method will be called after `connect` is called. After this method finishes, the writer will be drained. Subclasses should make use of this if they need to send data to Telegram to indicate which connection mode will be used. """ if self._codec.tag: self._writer.write(self._codec.tag) def _send(self, data): self._writer.write(self._codec.encode_packet(data)) async def _recv(self): return await self._codec.read_packet(self._reader) def __str__(self): return '{}:{}/{}'.format( self._ip, self._port, self.__class__.__name__.replace('Connection', '') ) class ObfuscatedConnection(Connection): """ Base class for "obfuscated" connections ("obfuscated2", "mtproto proxy") """ """ This attribute should be redefined by subclasses """ obfuscated_io = None def _init_conn(self): self._obfuscation = self.obfuscated_io(self) self._writer.write(self._obfuscation.header) def _send(self, data): self._obfuscation.write(self._codec.encode_packet(data)) async def _recv(self): return await self._codec.read_packet(self._obfuscation) class PacketCodec(abc.ABC): """ Base class for packet codecs """ """ This attribute should be re-defined by subclass to define if some "magic bytes" should be sent to server right after connection is made to signal which protocol will be used """ tag = None def __init__(self, connection): """ Codec is created when connection is just made. """ self._conn = connection @abc.abstractmethod def encode_packet(self, data): """ Encodes single packet and returns encoded bytes. """ raise NotImplementedError @abc.abstractmethod async def read_packet(self, reader): """ Reads single packet from `reader` object that should have `readexactly(n)` method. """ raise NotImplementedError Telethon-1.39.0/telethon/network/connection/http.py000066400000000000000000000023041475566265000224100ustar00rootroot00000000000000import asyncio from .connection import Connection, PacketCodec SSL_PORT = 443 class HttpPacketCodec(PacketCodec): tag = None obfuscate_tag = None def encode_packet(self, data): return ('POST /api HTTP/1.1\r\n' 'Host: {}:{}\r\n' 'Content-Type: application/x-www-form-urlencoded\r\n' 'Connection: keep-alive\r\n' 'Keep-Alive: timeout=100000, max=10000000\r\n' 'Content-Length: {}\r\n\r\n' .format(self._conn._ip, self._conn._port, len(data)) .encode('ascii') + data) async def read_packet(self, reader): while True: line = await reader.readline() if not line or line[-1] != b'\n': raise asyncio.IncompleteReadError(line, None) if line.lower().startswith(b'content-length: '): await reader.readexactly(2) length = int(line[16:-2]) return await reader.readexactly(length) class ConnectionHttp(Connection): packet_codec = HttpPacketCodec async def connect(self, timeout=None, ssl=None): await super().connect(timeout=timeout, ssl=self._port == SSL_PORT) Telethon-1.39.0/telethon/network/connection/tcpabridged.py000066400000000000000000000017011475566265000237010ustar00rootroot00000000000000import struct from .connection import Connection, PacketCodec class AbridgedPacketCodec(PacketCodec): tag = b'\xef' obfuscate_tag = b'\xef\xef\xef\xef' def encode_packet(self, data): length = len(data) >> 2 if length < 127: length = struct.pack('B', length) else: length = b'\x7f' + int.to_bytes(length, 3, 'little') return length + data async def read_packet(self, reader): length = struct.unpack('= 127: length = struct.unpack( ' 0: return packet_with_padding[:-pad_size] return packet_with_padding class ConnectionTcpIntermediate(Connection): """ Intermediate mode between `ConnectionTcpFull` and `ConnectionTcpAbridged`. Always sends 4 extra bytes for the packet length. """ packet_codec = IntermediatePacketCodec Telethon-1.39.0/telethon/network/connection/tcpmtproxy.py000066400000000000000000000131731475566265000236700ustar00rootroot00000000000000import asyncio import hashlib import base64 import os from .connection import ObfuscatedConnection from .tcpabridged import AbridgedPacketCodec from .tcpintermediate import ( IntermediatePacketCodec, RandomizedIntermediatePacketCodec ) from ...crypto import AESModeCTR class MTProxyIO: """ It's very similar to tcpobfuscated.ObfuscatedIO, but the way encryption keys, protocol tag and dc_id are encoded is different. """ header = None def __init__(self, connection): self._reader = connection._reader self._writer = connection._writer (self.header, self._encrypt, self._decrypt) = self.init_header( connection._secret, connection._dc_id, connection.packet_codec) @staticmethod def init_header(secret, dc_id, packet_codec): # Validate is_dd = (len(secret) == 17) and (secret[0] == 0xDD) is_rand_codec = issubclass( packet_codec, RandomizedIntermediatePacketCodec) if is_dd and not is_rand_codec: raise ValueError( "Only RandomizedIntermediate can be used with dd-secrets") secret = secret[1:] if is_dd else secret if len(secret) != 16: raise ValueError( "MTProxy secret must be a hex-string representing 16 bytes") # Obfuscated messages secrets cannot start with any of these keywords = (b'PVrG', b'GET ', b'POST', b'\xee\xee\xee\xee') while True: random = os.urandom(64) if (random[0] != 0xef and random[:4] not in keywords and random[4:4] != b'\0\0\0\0'): break random = bytearray(random) random_reversed = random[55:7:-1] # Reversed (8, len=48) # Encryption has "continuous buffer" enabled encrypt_key = hashlib.sha256( bytes(random[8:40]) + secret).digest() encrypt_iv = bytes(random[40:56]) decrypt_key = hashlib.sha256( bytes(random_reversed[:32]) + secret).digest() decrypt_iv = bytes(random_reversed[32:48]) encryptor = AESModeCTR(encrypt_key, encrypt_iv) decryptor = AESModeCTR(decrypt_key, decrypt_iv) random[56:60] = packet_codec.obfuscate_tag dc_id_bytes = dc_id.to_bytes(2, "little", signed=True) random = random[:60] + dc_id_bytes + random[62:] random[56:64] = encryptor.encrypt(bytes(random))[56:64] return (random, encryptor, decryptor) async def readexactly(self, n): return self._decrypt.encrypt(await self._reader.readexactly(n)) def write(self, data): self._writer.write(self._encrypt.encrypt(data)) class TcpMTProxy(ObfuscatedConnection): """ Connector which allows user to connect to the Telegram via proxy servers commonly known as MTProxy. Implemented very ugly due to the leaky abstractions in Telethon networking classes that should be refactored later (TODO). .. warning:: The support for TcpMTProxy classes is **EXPERIMENTAL** and prone to be changed. You shouldn't be using this class yet. """ packet_codec = None obfuscated_io = MTProxyIO # noinspection PyUnusedLocal def __init__(self, ip, port, dc_id, *, loggers, proxy=None, local_addr=None): # connect to proxy's host and port instead of telegram's ones proxy_host, proxy_port = self.address_info(proxy) self._secret = self.normalize_secret(proxy[2]) super().__init__( proxy_host, proxy_port, dc_id, loggers=loggers) async def _connect(self, timeout=None, ssl=None): await super()._connect(timeout=timeout, ssl=ssl) # Wait for EOF for 2 seconds (or if _wait_for_data's definition # is missing or different, just sleep for 2 seconds). This way # we give the proxy a chance to close the connection if the current # codec (which the proxy detects with the data we sent) cannot # be used for this proxy. This is a work around for #1134. # TODO Sleeping for N seconds may not be the best solution # TODO This fix could be welcome for HTTP proxies as well try: await asyncio.wait_for(self._reader._wait_for_data('proxy'), 2) except asyncio.TimeoutError: pass except Exception: await asyncio.sleep(2) if self._reader.at_eof(): await self.disconnect() raise ConnectionError( 'Proxy closed the connection after sending initial payload') @staticmethod def address_info(proxy_info): if proxy_info is None: raise ValueError("No proxy info specified for MTProxy connection") return proxy_info[:2] @staticmethod def normalize_secret(secret): if secret[:2] in ("ee", "dd"): # Remove extra bytes secret = secret[2:] try: secret_bytes = bytes.fromhex(secret) except ValueError: secret = secret + '=' * (-len(secret) % 4) secret_bytes = base64.b64decode(secret.encode()) return secret_bytes[:16] # Remove the domain from the secret (until domain support is added) class ConnectionTcpMTProxyAbridged(TcpMTProxy): """ Connect to proxy using abridged protocol """ packet_codec = AbridgedPacketCodec class ConnectionTcpMTProxyIntermediate(TcpMTProxy): """ Connect to proxy using intermediate protocol """ packet_codec = IntermediatePacketCodec class ConnectionTcpMTProxyRandomizedIntermediate(TcpMTProxy): """ Connect to proxy using randomized intermediate protocol (dd-secrets) """ packet_codec = RandomizedIntermediatePacketCodec Telethon-1.39.0/telethon/network/connection/tcpobfuscated.py000066400000000000000000000037231475566265000242650ustar00rootroot00000000000000import os from .tcpabridged import AbridgedPacketCodec from .connection import ObfuscatedConnection from ...crypto import AESModeCTR class ObfuscatedIO: header = None def __init__(self, connection): self._reader = connection._reader self._writer = connection._writer (self.header, self._encrypt, self._decrypt) = self.init_header(connection.packet_codec) @staticmethod def init_header(packet_codec): # Obfuscated messages secrets cannot start with any of these keywords = (b'PVrG', b'GET ', b'POST', b'\xee\xee\xee\xee') while True: random = os.urandom(64) if (random[0] != 0xef and random[:4] not in keywords and random[4:8] != b'\0\0\0\0'): break random = bytearray(random) random_reversed = random[55:7:-1] # Reversed (8, len=48) # Encryption has "continuous buffer" enabled encrypt_key = bytes(random[8:40]) encrypt_iv = bytes(random[40:56]) decrypt_key = bytes(random_reversed[:32]) decrypt_iv = bytes(random_reversed[32:48]) encryptor = AESModeCTR(encrypt_key, encrypt_iv) decryptor = AESModeCTR(decrypt_key, decrypt_iv) random[56:60] = packet_codec.obfuscate_tag random[56:64] = encryptor.encrypt(bytes(random))[56:64] return (random, encryptor, decryptor) async def readexactly(self, n): return self._decrypt.encrypt(await self._reader.readexactly(n)) def write(self, data): self._writer.write(self._encrypt.encrypt(data)) class ConnectionTcpObfuscated(ObfuscatedConnection): """ Mode that Telegram defines as "obfuscated2". Encodes the packet just like `ConnectionTcpAbridged`, but encrypts every message with a randomly generated key using the AES-CTR mode so the packets are harder to discern. """ obfuscated_io = ObfuscatedIO packet_codec = AbridgedPacketCodec Telethon-1.39.0/telethon/network/mtprotoplainsender.py000066400000000000000000000037431475566265000232330ustar00rootroot00000000000000""" 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 from .mtprotostate import MTProtoState from ..errors import InvalidBufferError from ..extensions import BinaryReader class MTProtoPlainSender: """ MTProto Mobile Protocol plain sender (https://core.telegram.org/mtproto/description#unencrypted-messages) """ def __init__(self, connection, *, loggers): """ Initializes the MTProto plain sender. :param connection: the Connection to be used. """ self._state = MTProtoState(auth_key=None, loggers=loggers) self._connection = connection async def send(self, request): """ Sends and receives the result for the given request. """ body = bytes(request) msg_id = self._state._get_new_msg_id() await self._connection.send( struct.pack(' 0, 'Bad length' # We could read length bytes and use those in a new reader to read # the next TLObject without including the padding, but since the # reader isn't used for anything else after this, it's unnecessary. return reader.tgread_object() Telethon-1.39.0/telethon/network/mtprotosender.py000066400000000000000000001133421475566265000222040ustar00rootroot00000000000000import asyncio import collections import struct import datetime import time from . import authenticator from ..extensions.messagepacker import MessagePacker from .mtprotoplainsender import MTProtoPlainSender from .requeststate import RequestState from .mtprotostate import MTProtoState from ..tl.tlobject import TLRequest from .. import helpers, utils from ..errors import ( BadMessageError, InvalidBufferError, AuthKeyNotFound, SecurityError, TypeNotFoundError, rpc_message_to_error ) from ..extensions import BinaryReader from ..tl.core import RpcResult, MessageContainer, GzipPacked from ..tl.functions.auth import LogOutRequest from ..tl.functions import PingRequest, DestroySessionRequest, DestroyAuthKeyRequest from ..tl.types import ( MsgsAck, Pong, BadServerSalt, BadMsgNotification, FutureSalts, MsgNewDetailedInfo, NewSessionCreated, MsgDetailedInfo, MsgsStateReq, MsgsStateInfo, MsgsAllInfo, MsgResendReq, upload, DestroySessionOk, DestroySessionNone, DestroyAuthKeyOk, DestroyAuthKeyNone, DestroyAuthKeyFail ) from ..tl import types as _tl from ..crypto import AuthKey from ..helpers import retry_range class MTProtoSender: """ MTProto Mobile Protocol sender (https://core.telegram.org/mtproto/description). This class is responsible for wrapping requests into `TLMessage`'s, sending them over the network and receiving them in a safe manner. Automatic reconnection due to temporary network issues is a concern for this class as well, including retry of messages that could not be sent successfully. A new authorization key will be generated on connection if no other key exists yet. """ def __init__(self, auth_key, *, loggers, retries=5, delay=1, auto_reconnect=True, connect_timeout=None, auth_key_callback=None, updates_queue=None, auto_reconnect_callback=None): self._connection = None self._loggers = loggers self._log = loggers[__name__] self._retries = retries self._delay = delay self._auto_reconnect = auto_reconnect self._connect_timeout = connect_timeout self._auth_key_callback = auth_key_callback self._updates_queue = updates_queue self._auto_reconnect_callback = auto_reconnect_callback self._connect_lock = asyncio.Lock() self._ping = None # Whether the user has explicitly connected or disconnected. # # If a disconnection happens for any other reason and it # was *not* user action then the pending messages won't # be cleared but on explicit user disconnection all the # pending futures should be cancelled. self._user_connected = False self._reconnecting = False self._disconnected = helpers.get_running_loop().create_future() self._disconnected.set_result(None) # We need to join the loops upon disconnection self._send_loop_handle = None self._recv_loop_handle = None # Preserving the references of the AuthKey and state is important self.auth_key = auth_key or AuthKey(None) self._state = MTProtoState(self.auth_key, loggers=self._loggers) # Outgoing messages are put in a queue and sent in a batch. # Note that here we're also storing their ``_RequestState``. self._send_queue = MessagePacker(self._state, loggers=self._loggers) # Sent states are remembered until a response is received. self._pending_state = {} # Responses must be acknowledged, and we can also batch these. self._pending_ack = set() # Similar to pending_messages but only for the last acknowledges. # These can't go in pending_messages because no acknowledge for them # is received, but we may still need to resend their state on bad salts. self._last_acks = collections.deque(maxlen=10) # Jump table from response ID to method that handles it self._handlers = { RpcResult.CONSTRUCTOR_ID: self._handle_rpc_result, MessageContainer.CONSTRUCTOR_ID: self._handle_container, GzipPacked.CONSTRUCTOR_ID: self._handle_gzip_packed, Pong.CONSTRUCTOR_ID: self._handle_pong, BadServerSalt.CONSTRUCTOR_ID: self._handle_bad_server_salt, BadMsgNotification.CONSTRUCTOR_ID: self._handle_bad_notification, MsgDetailedInfo.CONSTRUCTOR_ID: self._handle_detailed_info, MsgNewDetailedInfo.CONSTRUCTOR_ID: self._handle_new_detailed_info, NewSessionCreated.CONSTRUCTOR_ID: self._handle_new_session_created, MsgsAck.CONSTRUCTOR_ID: self._handle_ack, FutureSalts.CONSTRUCTOR_ID: self._handle_future_salts, MsgsStateReq.CONSTRUCTOR_ID: self._handle_state_forgotten, MsgResendReq.CONSTRUCTOR_ID: self._handle_state_forgotten, MsgsAllInfo.CONSTRUCTOR_ID: self._handle_msg_all, DestroySessionOk.CONSTRUCTOR_ID: self._handle_destroy_session, DestroySessionNone.CONSTRUCTOR_ID: self._handle_destroy_session, DestroyAuthKeyOk.CONSTRUCTOR_ID: self._handle_destroy_auth_key, DestroyAuthKeyNone.CONSTRUCTOR_ID: self._handle_destroy_auth_key, DestroyAuthKeyFail.CONSTRUCTOR_ID: self._handle_destroy_auth_key, } # Public API async def connect(self, connection): """ Connects to the specified given connection using the given auth key. """ async with self._connect_lock: if self._user_connected: self._log.info('User is already connected!') return False self._connection = connection await self._connect() self._user_connected = True return True def is_connected(self): return self._user_connected def _transport_connected(self): return ( not self._reconnecting and self._connection is not None and self._connection._connected ) async def disconnect(self): """ Cleanly disconnects the instance from the network, cancels all pending requests, and closes the send and receive loops. """ await self._disconnect() def send(self, request, ordered=False): """ This method enqueues the given request to be sent. Its send state will be saved until a response arrives, and a ``Future`` that will be resolved when the response arrives will be returned: .. code-block:: python async def method(): # Sending (enqueued for the send loop) future = sender.send(request) # Receiving (waits for the receive loop to read the result) result = await future Designed like this because Telegram may send the response at any point, and it can send other items while one waits for it. Once the response for this future arrives, it is set with the received result, quite similar to how a ``receive()`` call would otherwise work. Since the receiving part is "built in" the future, it's impossible to await receive a result that was never sent. """ if not self._user_connected: raise ConnectionError('Cannot send requests while disconnected') if not utils.is_list_like(request): try: state = RequestState(request) except struct.error as e: # "struct.error: required argument is not an integer" is not # very helpful; log the request to find out what wasn't int. self._log.error('Request caused struct.error: %s: %s', e, request) raise self._send_queue.append(state) return state.future else: states = [] futures = [] state = None for req in request: try: state = RequestState(req, after=ordered and state) except struct.error as e: self._log.error('Request caused struct.error: %s: %s', e, request) raise states.append(state) futures.append(state.future) self._send_queue.extend(states) return futures @property def disconnected(self): """ Future that resolves when the connection to Telegram ends, either by user action or in the background. Note that it may resolve in either a ``ConnectionError`` or any other unexpected error that could not be handled. """ return asyncio.shield(self._disconnected) # Private methods async def _connect(self): """ Performs the actual connection, retrying, generating the authorization key if necessary, and starting the send and receive loops. """ self._log.info('Connecting to %s...', self._connection) connected = False for attempt in retry_range(self._retries): if not connected: connected = await self._try_connect(attempt) if not connected: continue # skip auth key generation until we're connected if not self.auth_key: try: if not await self._try_gen_auth_key(attempt): continue # keep retrying until we have the auth key except (IOError, asyncio.TimeoutError) as e: # Sometimes, specially during user-DC migrations, # Telegram may close the connection during auth_key # generation. If that's the case, we will need to # connect again. self._log.warning('Connection error %d during auth_key gen: %s: %s', attempt, type(e).__name__, e) # Whatever the IOError was, make sure to disconnect so we can # reconnect cleanly after. await self._connection.disconnect() connected = False await asyncio.sleep(self._delay) continue # next iteration we will try to reconnect break # all steps done, break retry loop else: if not connected: raise ConnectionError('Connection to Telegram failed {} time(s)'.format(self._retries)) e = ConnectionError('auth_key generation failed {} time(s)'.format(self._retries)) await self._disconnect(error=e) raise e loop = helpers.get_running_loop() self._log.debug('Starting send loop') self._send_loop_handle = loop.create_task(self._send_loop()) self._log.debug('Starting receive loop') self._recv_loop_handle = loop.create_task(self._recv_loop()) # _disconnected only completes after manual disconnection # or errors after which the sender cannot continue such # as failing to reconnect or any unexpected error. if self._disconnected.done(): self._disconnected = loop.create_future() self._log.info('Connection to %s complete!', self._connection) async def _try_connect(self, attempt): try: self._log.debug('Connection attempt %d...', attempt) await self._connection.connect(timeout=self._connect_timeout) self._log.debug('Connection success!') return True except (IOError, asyncio.TimeoutError) as e: self._log.warning('Attempt %d at connecting failed: %s: %s', attempt, type(e).__name__, e) await asyncio.sleep(self._delay) return False async def _try_gen_auth_key(self, attempt): plain = MTProtoPlainSender(self._connection, loggers=self._loggers) try: self._log.debug('New auth_key attempt %d...', attempt) self.auth_key.key, self._state.time_offset = \ await authenticator.do_authentication(plain) # This is *EXTREMELY* important since we don't control # external references to the authorization key, we must # notify whenever we change it. This is crucial when we # switch to different data centers. if self._auth_key_callback: self._auth_key_callback(self.auth_key) self._log.debug('auth_key generation success!') return True except (SecurityError, AssertionError) as e: self._log.warning('Attempt %d at new auth_key failed: %s', attempt, e) await asyncio.sleep(self._delay) return False async def _disconnect(self, error=None): if self._connection is None: self._log.info('Not disconnecting (already have no connection)') return self._log.info('Disconnecting from %s...', self._connection) self._user_connected = False try: self._log.debug('Closing current connection...') await self._connection.disconnect() finally: self._log.debug('Cancelling %d pending message(s)...', len(self._pending_state)) for state in self._pending_state.values(): if error and not state.future.done(): state.future.set_exception(error) else: state.future.cancel() self._pending_state.clear() await helpers._cancel( self._log, send_loop_handle=self._send_loop_handle, recv_loop_handle=self._recv_loop_handle ) self._log.info('Disconnection from %s complete!', self._connection) self._connection = None if self._disconnected and not self._disconnected.done(): if error: self._disconnected.set_exception(error) else: self._disconnected.set_result(None) async def _reconnect(self, last_error): """ Cleanly disconnects and then reconnects. """ self._log.info('Closing current connection to begin reconnect...') await self._connection.disconnect() await helpers._cancel( self._log, send_loop_handle=self._send_loop_handle, recv_loop_handle=self._recv_loop_handle ) # TODO See comment in `_start_reconnect` # Perhaps this should be the last thing to do? # But _connect() creates tasks which may run and, # if they see that reconnecting is True, they will end. # Perhaps that task creation should not belong in connect? self._reconnecting = False # Start with a clean state (and thus session ID) to avoid old msgs self._state.reset() retries = self._retries if self._auto_reconnect else 0 attempt = 0 ok = True # We're already "retrying" to connect, so we don't want to force retries for attempt in retry_range(retries, force_retry=False): try: await self._connect() except (IOError, asyncio.TimeoutError) as e: last_error = e self._log.info('Failed reconnection attempt %d with %s', attempt, e.__class__.__name__) await asyncio.sleep(self._delay) except BufferError as e: # TODO there should probably only be one place to except all these errors if isinstance(e, InvalidBufferError) and e.code == 404: self._log.info('Server does not know about the current auth key; the session may need to be recreated') last_error = AuthKeyNotFound() ok = False break else: self._log.warning('Invalid buffer %s', e) except Exception as e: last_error = e self._log.exception('Unexpected exception reconnecting on ' 'attempt %d', attempt) await asyncio.sleep(self._delay) else: self._send_queue.extend(self._pending_state.values()) self._pending_state.clear() if self._auto_reconnect_callback: helpers.get_running_loop().create_task(self._auto_reconnect_callback()) break else: ok = False if not ok: self._log.error('Automatic reconnection failed %d time(s)', attempt) # There may be no error (e.g. automatic reconnection was turned off). error = last_error.with_traceback(None) if last_error else None await self._disconnect(error=error) def _start_reconnect(self, error): """Starts a reconnection in the background.""" if self._user_connected and not self._reconnecting: # We set reconnecting to True here and not inside the new task # because it may happen that send/recv loop calls this again # while the new task hasn't had a chance to run yet. This race # condition puts `self.connection` in a bad state with two calls # to its `connect` without disconnecting, so it creates a second # receive loop. There can't be two tasks receiving data from # the reader, since that causes an error, and the library just # gets stuck. # TODO It still gets stuck? Investigate where and why. self._reconnecting = True helpers.get_running_loop().create_task(self._reconnect(error)) def _keepalive_ping(self, rnd_id): """ Send a keep-alive ping. If a pong for the last ping was not received yet, this means we're probably not connected. """ # TODO this is ugly, update loop shouldn't worry about this, sender should if self._ping is None: self._ping = rnd_id self.send(PingRequest(rnd_id)) else: self._start_reconnect(None) # Loops async def _send_loop(self): """ This loop is responsible for popping items off the send queue, encrypting them, and sending them over the network. Besides `connect`, only this method ever sends data. """ while self._user_connected and not self._reconnecting: if self._pending_ack: ack = RequestState(MsgsAck(list(self._pending_ack))) self._send_queue.append(ack) self._last_acks.append(ack) self._pending_ack.clear() self._log.debug('Waiting for messages to send...') # TODO Wait for the connection send queue to be empty? # This means that while it's not empty we can wait for # more messages to be added to the send queue. batch, data = await self._send_queue.get() if not data: continue self._log.debug('Encrypting %d message(s) in %d bytes for sending', len(batch), len(data)) data = self._state.encrypt_message_data(data) # Whether sending succeeds or not, the popped requests are now # pending because they're removed from the queue. If a reconnect # occurs, they will be removed from pending state and re-enqueued # so even if the network fails they won't be lost. If they were # never re-enqueued, the future waiting for a response "locks". for state in batch: if not isinstance(state, list): if isinstance(state.request, TLRequest): self._pending_state[state.msg_id] = state else: for s in state: if isinstance(s.request, TLRequest): self._pending_state[s.msg_id] = s try: await self._connection.send(data) except IOError as e: self._log.info('Connection closed while sending data') self._start_reconnect(e) return self._log.debug('Encrypted messages put in a queue to be sent') async def _recv_loop(self): """ This loop is responsible for reading all incoming responses from the network, decrypting and handling or dispatching them. Besides `connect`, only this method ever receives data. """ while self._user_connected and not self._reconnecting: self._log.debug('Receiving items from the network...') try: body = await self._connection.recv() except asyncio.CancelledError: raise # bypass except Exception except (IOError, asyncio.IncompleteReadError) as e: self._log.info('Connection closed while receiving data: %s', e) self._start_reconnect(e) return except InvalidBufferError as e: if e.code == 429: self._log.warning('Server indicated flood error at transport level: %s', e) await self._disconnect(error=e) else: self._log.exception('Server sent invalid buffer') self._start_reconnect(e) return except Exception as e: self._log.exception('Unhandled error while receiving data') self._start_reconnect(e) return try: message = self._state.decrypt_message_data(body) if message is None: continue # this message is to be ignored except TypeNotFoundError as e: # Received object which we don't know how to deserialize self._log.info('Type %08x not found, remaining data %r', e.invalid_constructor_id, e.remaining) continue except SecurityError as e: # A step while decoding had the incorrect data. This message # should not be considered safe and it should be ignored. self._log.warning('Security error while unpacking a ' 'received message: %s', e) continue except BufferError as e: if isinstance(e, InvalidBufferError) and e.code == 404: self._log.info('Server does not know about the current auth key; the session may need to be recreated') await self._disconnect(error=AuthKeyNotFound()) else: self._log.warning('Invalid buffer %s', e) self._start_reconnect(e) return except Exception as e: self._log.exception('Unhandled error while decrypting data') self._start_reconnect(e) return try: await self._process_message(message) except Exception: self._log.exception('Unhandled error while processing msgs') # Response Handlers async def _process_message(self, message): """ Adds the given message to the list of messages that must be acknowledged and dispatches control to different ``_handle_*`` method based on its type. """ self._pending_ack.add(message.msg_id) handler = self._handlers.get(message.obj.CONSTRUCTOR_ID, self._handle_update) await handler(message) def _pop_states(self, msg_id): """ Pops the states known to match the given ID from pending messages. This method should be used when the response isn't specific. """ state = self._pending_state.pop(msg_id, None) if state: return [state] to_pop = [] for state in self._pending_state.values(): if state.container_id == msg_id: to_pop.append(state.msg_id) if to_pop: return [self._pending_state.pop(x) for x in to_pop] for ack in self._last_acks: if ack.msg_id == msg_id: return [ack] return [] async def _handle_rpc_result(self, message): """ Handles the result for Remote Procedure Calls: rpc_result#f35c6d01 req_msg_id:long result:bytes = RpcResult; This is where the future results for sent requests are set. """ rpc_result = message.obj state = self._pending_state.pop(rpc_result.req_msg_id, None) self._log.debug('Handling RPC result for message %d', rpc_result.req_msg_id) if not state: # TODO We should not get responses to things we never sent # However receiving a File() with empty bytes is "common". # See #658, #759 and #958. They seem to happen in a container # which contain the real response right after. # # But, it might also happen that we get an *error* for no parent request. # If that's the case attempting to read from body which is None would fail with: # "BufferError: No more data left to read (need 4, got 0: b''); last read None". # This seems to be particularly common for "RpcError(error_code=-500, error_message='No workers running')". if rpc_result.error: self._log.info('Received error without parent request: %s', rpc_result.error) else: try: with BinaryReader(rpc_result.body) as reader: if not isinstance(reader.tgread_object(), upload.File): raise ValueError('Not an upload.File') except (TypeNotFoundError, ValueError): self._log.info('Received response without parent request: %s', rpc_result.body) return if rpc_result.error: error = rpc_message_to_error(rpc_result.error, state.request) self._send_queue.append( RequestState(MsgsAck([state.msg_id]))) if not state.future.cancelled(): state.future.set_exception(error) else: try: with BinaryReader(rpc_result.body) as reader: result = state.request.read_result(reader) except Exception as e: # e.g. TypeNotFoundError, should be propagated to caller if not state.future.cancelled(): state.future.set_exception(e) else: self._store_own_updates(result) if not state.future.cancelled(): state.future.set_result(result) async def _handle_container(self, message): """ Processes the inner messages of a container with many of them: msg_container#73f1f8dc messages:vector<%Message> = MessageContainer; """ self._log.debug('Handling container') for inner_message in message.obj.messages: await self._process_message(inner_message) async def _handle_gzip_packed(self, message): """ Unpacks the data from a gzipped object and processes it: gzip_packed#3072cfa1 packed_data:bytes = Object; """ self._log.debug('Handling gzipped data') with BinaryReader(message.obj.data) as reader: message.obj = reader.tgread_object() await self._process_message(message) async def _handle_update(self, message): try: assert message.obj.SUBCLASS_OF_ID == 0x8af52aac # crc32(b'Updates') except AssertionError: self._log.warning( 'Note: %s is not an update, not dispatching it %s', message.obj.__class__.__name__, message.obj ) return self._log.debug('Handling update %s', message.obj.__class__.__name__) self._updates_queue.put_nowait(message.obj) def _store_own_updates(self, obj, *, _update_ids=frozenset(( _tl.UpdateShortMessage.CONSTRUCTOR_ID, _tl.UpdateShortChatMessage.CONSTRUCTOR_ID, _tl.UpdateShort.CONSTRUCTOR_ID, _tl.UpdatesCombined.CONSTRUCTOR_ID, _tl.Updates.CONSTRUCTOR_ID, _tl.UpdateShortSentMessage.CONSTRUCTOR_ID, )), _update_like_ids=frozenset(( _tl.messages.AffectedHistory.CONSTRUCTOR_ID, _tl.messages.AffectedMessages.CONSTRUCTOR_ID, _tl.messages.AffectedFoundMessages.CONSTRUCTOR_ID, ))): try: if obj.CONSTRUCTOR_ID in _update_ids: obj._self_outgoing = True # flag to only process, but not dispatch these self._updates_queue.put_nowait(obj) elif obj.CONSTRUCTOR_ID in _update_like_ids: # Ugly "hack" (?) - otherwise bots reliably detect gaps when deleting messages. # # Note: the `date` being `None` is used to check for `updatesTooLong`, so epoch # is used instead. It is still not read, because `updateShort` has no `seq`. # # Some requests, such as `readHistory`, also return these types. But the `pts_count` # seems to be zero, so while this will produce some bogus `updateDeleteMessages`, # it's still one of the "cleaner" approaches to handling the new `pts`. # `updateDeleteMessages` is probably the "least-invasive" update that can be used. upd = _tl.UpdateShort( _tl.UpdateDeleteMessages([], obj.pts, obj.pts_count), datetime.datetime(*time.gmtime(0)[:6]).replace(tzinfo=datetime.timezone.utc) ) upd._self_outgoing = True self._updates_queue.put_nowait(upd) elif obj.CONSTRUCTOR_ID == _tl.messages.InvitedUsers.CONSTRUCTOR_ID: obj.updates._self_outgoing = True self._updates_queue.put_nowait(obj.updates) except AttributeError: pass async def _handle_pong(self, message): """ Handles pong results, which don't come inside a ``rpc_result`` but are still sent through a request: pong#347773c5 msg_id:long ping_id:long = Pong; """ pong = message.obj self._log.debug('Handling pong for message %d', pong.msg_id) if self._ping == pong.ping_id: self._ping = None state = self._pending_state.pop(pong.msg_id, None) if state: state.future.set_result(pong) async def _handle_bad_server_salt(self, message): """ Corrects the currently used server salt to use the right value before enqueuing the rejected message to be re-sent: bad_server_salt#edab447b bad_msg_id:long bad_msg_seqno:int error_code:int new_server_salt:long = BadMsgNotification; """ bad_salt = message.obj self._log.debug('Handling bad salt for message %d', bad_salt.bad_msg_id) self._state.salt = bad_salt.new_server_salt states = self._pop_states(bad_salt.bad_msg_id) self._send_queue.extend(states) self._log.debug('%d message(s) will be resent', len(states)) async def _handle_bad_notification(self, message): """ Adjusts the current state to be correct based on the received bad message notification whenever possible: bad_msg_notification#a7eff811 bad_msg_id:long bad_msg_seqno:int error_code:int = BadMsgNotification; """ bad_msg = message.obj states = self._pop_states(bad_msg.bad_msg_id) self._log.debug('Handling bad msg %s', bad_msg) if bad_msg.error_code in (16, 17): # Sent msg_id too low or too high (respectively). # Use the current msg_id to determine the right time offset. to = self._state.update_time_offset( correct_msg_id=message.msg_id) self._log.info('System clock is wrong, set time offset to %ds', to) elif bad_msg.error_code == 32: # msg_seqno too low, so just pump it up by some "large" amount # TODO A better fix would be to start with a new fresh session ID self._state._sequence += 64 elif bad_msg.error_code == 33: # msg_seqno too high never seems to happen but just in case self._state._sequence -= 16 else: for state in states: state.future.set_exception( BadMessageError(state.request, bad_msg.error_code)) return # Messages are to be re-sent once we've corrected the issue self._send_queue.extend(states) self._log.debug('%d messages will be resent due to bad msg', len(states)) async def _handle_detailed_info(self, message): """ Updates the current status with the received detailed information: msg_detailed_info#276d3ec6 msg_id:long answer_msg_id:long bytes:int status:int = MsgDetailedInfo; """ # TODO https://goo.gl/VvpCC6 msg_id = message.obj.answer_msg_id self._log.debug('Handling detailed info for message %d', msg_id) self._pending_ack.add(msg_id) async def _handle_new_detailed_info(self, message): """ Updates the current status with the received detailed information: msg_new_detailed_info#809db6df answer_msg_id:long bytes:int status:int = MsgDetailedInfo; """ # TODO https://goo.gl/G7DPsR msg_id = message.obj.answer_msg_id self._log.debug('Handling new detailed info for message %d', msg_id) self._pending_ack.add(msg_id) async def _handle_new_session_created(self, message): """ Updates the current status with the received session information: new_session_created#9ec20908 first_msg_id:long unique_id:long server_salt:long = NewSession; """ # TODO https://goo.gl/LMyN7A self._log.debug('Handling new session created') self._state.salt = message.obj.server_salt async def _handle_ack(self, message): """ Handles a server acknowledge about our messages. Normally these can be ignored except in the case of ``auth.logOut``: auth.logOut#5717da40 = Bool; Telegram doesn't seem to send its result so we need to confirm it manually. No other request is known to have this behaviour. Since the ID of sent messages consisting of a container is never returned (unless on a bad notification), this method also removes containers messages when any of their inner messages are acknowledged. """ ack = message.obj self._log.debug('Handling acknowledge for %s', str(ack.msg_ids)) for msg_id in ack.msg_ids: state = self._pending_state.get(msg_id) if state and isinstance(state.request, LogOutRequest): del self._pending_state[msg_id] if not state.future.cancelled(): state.future.set_result(True) async def _handle_future_salts(self, message): """ Handles future salt results, which don't come inside a ``rpc_result`` but are still sent through a request: future_salts#ae500895 req_msg_id:long now:int salts:vector = FutureSalts; """ # TODO save these salts and automatically adjust to the # correct one whenever the salt in use expires. self._log.debug('Handling future salts for message %d', message.msg_id) state = self._pending_state.pop(message.msg_id, None) if state: state.future.set_result(message.obj) async def _handle_state_forgotten(self, message): """ Handles both :tl:`MsgsStateReq` and :tl:`MsgResendReq` by enqueuing a :tl:`MsgsStateInfo` to be sent at a later point. """ self._send_queue.append(RequestState(MsgsStateInfo( req_msg_id=message.msg_id, info=chr(1) * len(message.obj.msg_ids) ))) async def _handle_msg_all(self, message): """ Handles :tl:`MsgsAllInfo` by doing nothing (yet). """ async def _handle_destroy_session(self, message): """ Handles both :tl:`DestroySessionOk` and :tl:`DestroySessionNone`. It behaves pretty much like handling an RPC result. """ for msg_id, state in self._pending_state.items(): if isinstance(state.request, DestroySessionRequest)\ and state.request.session_id == message.obj.session_id: break else: return del self._pending_state[msg_id] if not state.future.cancelled(): state.future.set_result(message.obj) async def _handle_destroy_auth_key(self, message): """ Handles :tl:`DestroyAuthKeyFail`, :tl:`DestroyAuthKeyNone`, and :tl:`DestroyAuthKeyOk`. :tl:`DestroyAuthKey` is not intended for users to use, but they still might, and the response won't come in `rpc_result`, so thhat's worked around here. """ self._log.debug('Handling destroy auth key %s', message.obj) for msg_id, state in list(self._pending_state.items()): if isinstance(state.request, DestroyAuthKeyRequest): del self._pending_state[msg_id] if not state.future.cancelled(): state.future.set_result(message.obj) # If the auth key has been destroyed, that pretty much means the # library can't continue as our auth key will no longer be found # on the server. # Even if the library didn't disconnect, the server would (and then # the library would reconnect and learn about auth key being invalid). if isinstance(message.obj, DestroyAuthKeyOk): await self._disconnect(error=AuthKeyNotFound()) Telethon-1.39.0/telethon/network/mtprotostate.py000066400000000000000000000253301475566265000220430ustar00rootroot00000000000000import os import struct import time from hashlib import sha256 from collections import deque from ..crypto import AES from ..errors import SecurityError, InvalidBufferError from ..extensions import BinaryReader from ..tl.core import TLMessage from ..tl.tlobject import TLRequest from ..tl.functions import InvokeAfterMsgRequest from ..tl.core.gzippacked import GzipPacked from ..tl.types import BadServerSalt, BadMsgNotification # N is not specified in https://core.telegram.org/mtproto/security_guidelines#checking-msg-id, but 500 is reasonable MAX_RECENT_MSG_IDS = 500 MSG_TOO_NEW_DELTA = 30 MSG_TOO_OLD_DELTA = 300 # Something must be wrong if we ignore too many messages at the same time MAX_CONSECUTIVE_IGNORED = 10 class _OpaqueRequest(TLRequest): """ Wraps a serialized request into a type that can be serialized again. """ def __init__(self, data: bytes): self.data = data def _bytes(self): return self.data class MTProtoState: """ `telethon.network.mtprotosender.MTProtoSender` needs to hold a state in order to be able to encrypt and decrypt incoming/outgoing messages, as well as generating the message IDs. Instances of this class hold together all the required information. It doesn't make sense to use `telethon.sessions.abstract.Session` for the sender because the sender should *not* be concerned about storing this information to disk, as one may create as many senders as they desire to any other data center, or some CDN. Using the same session for all these is not a good idea as each need their own authkey, and the concept of "copying" sessions with the unnecessary entities or updates state for these connections doesn't make sense. While it would be possible to have a `MTProtoPlainState` that does no encryption so that it was usable through the `MTProtoLayer` and thus avoid the need for a `MTProtoPlainSender`, the `MTProtoLayer` is more focused to efficiency and this state is also more advanced (since it supports gzipping and invoking after other message IDs). There are too many methods that would be needed to make it convenient to use for the authentication process, at which point the `MTProtoPlainSender` is better. """ def __init__(self, auth_key, loggers): self.auth_key = auth_key self._log = loggers[__name__] self.time_offset = 0 self.salt = 0 self.id = self._sequence = self._last_msg_id = None self._recent_remote_ids = deque(maxlen=MAX_RECENT_MSG_IDS) self._highest_remote_id = 0 self._ignore_count = 0 self.reset() def reset(self): """ Resets the state. """ # Session IDs can be random on every connection self.id = struct.unpack('q', os.urandom(8))[0] self._sequence = 0 self._last_msg_id = 0 self._recent_remote_ids.clear() self._highest_remote_id = 0 self._ignore_count = 0 def update_message_id(self, message): """ Updates the message ID to a new one, used when the time offset changed. """ message.msg_id = self._get_new_msg_id() @staticmethod def _calc_key(auth_key, msg_key, client): """ Calculate the key based on Telegram guidelines for MTProto 2, specifying whether it's the client or not. See https://core.telegram.org/mtproto/description#defining-aes-key-and-initialization-vector """ x = 0 if client else 8 sha256a = sha256(msg_key + auth_key[x: x + 36]).digest() sha256b = sha256(auth_key[x + 40:x + 76] + msg_key).digest() aes_key = sha256a[:8] + sha256b[8:24] + sha256a[24:32] aes_iv = sha256b[:8] + sha256a[8:24] + sha256b[24:32] return aes_key, aes_iv def write_data_as_message(self, buffer, data, content_related, *, after_id=None): """ Writes a message containing the given data into buffer. Returns the message id. """ msg_id = self._get_new_msg_id() seq_no = self._get_seq_no(content_related) if after_id is None: body = GzipPacked.gzip_if_smaller(content_related, data) else: # The `RequestState` stores `bytes(request)`, not the request itself. # `invokeAfterMsg` wants a `TLRequest` though, hence the wrapping. body = GzipPacked.gzip_if_smaller(content_related, bytes(InvokeAfterMsgRequest(after_id, _OpaqueRequest(data)))) buffer.write(struct.pack('> 32 time_delta = now - remote_msg_time if time_delta > MSG_TOO_OLD_DELTA: self._log.warning('Server sent a very old message with ID %d, ignoring (see FAQ for details)', remote_msg_id) self._count_ignored() return None if -time_delta > MSG_TOO_NEW_DELTA: self._log.warning('Server sent a very new message with ID %d, ignoring (see FAQ for details)', remote_msg_id) self._count_ignored() return None self._recent_remote_ids.append(remote_msg_id) self._highest_remote_id = remote_msg_id self._ignore_count = 0 return TLMessage(remote_msg_id, remote_sequence, obj) def _count_ignored(self): # It's possible that ignoring a message "bricks" the connection, # but this should not happen unless there's something else wrong. self._ignore_count += 1 if self._ignore_count >= MAX_CONSECUTIVE_IGNORED: raise SecurityError('Too many messages had to be ignored consecutively') def _get_new_msg_id(self): """ Generates a new unique message ID based on the current time (in ms) since epoch, applying a known time offset. """ now = time.time() + self.time_offset nanoseconds = int((now - int(now)) * 1e+9) new_msg_id = (int(now) << 32) | (nanoseconds << 2) if self._last_msg_id >= new_msg_id: new_msg_id = self._last_msg_id + 4 self._last_msg_id = new_msg_id return new_msg_id def update_time_offset(self, correct_msg_id): """ Updates the time offset to the correct one given a known valid message ID. """ bad = self._get_new_msg_id() old = self.time_offset now = int(time.time()) correct = correct_msg_id >> 32 self.time_offset = correct - now if self.time_offset != old: self._last_msg_id = 0 self._log.debug( 'Updated time offset (old offset %d, bad %d, good %d, new %d)', old, bad, correct_msg_id, self.time_offset ) return self.time_offset def _get_seq_no(self, content_related): """ Generates the next sequence number depending on whether it should be for a content-related query or not. """ if content_related: result = self._sequence * 2 + 1 self._sequence += 1 return result else: return self._sequence * 2 Telethon-1.39.0/telethon/network/requeststate.py000066400000000000000000000012041475566265000220210ustar00rootroot00000000000000import asyncio class RequestState: """ This request state holds several information relevant to sent messages, in particular the message ID assigned to the request, the container ID it belongs to, the request itself, the request as bytes, and the future result that will eventually be resolved. """ __slots__ = ('container_id', 'msg_id', 'request', 'data', 'future', 'after') def __init__(self, request, after=None): self.container_id = None self.msg_id = None self.request = request self.data = bytes(request) self.future = asyncio.Future() self.after = after Telethon-1.39.0/telethon/password.py000066400000000000000000000160321475566265000174460ustar00rootroot00000000000000import hashlib import os from .crypto import factorization from .tl import types def check_prime_and_good_check(prime: int, g: int): good_prime_bits_count = 2048 if prime < 0 or prime.bit_length() != good_prime_bits_count: raise ValueError('bad prime count {}, expected {}' .format(prime.bit_length(), good_prime_bits_count)) # TODO This is awfully slow if factorization.Factorization.factorize(prime)[0] != 1: raise ValueError('given "prime" is not prime') if g == 2: if prime % 8 != 7: raise ValueError('bad g {}, mod8 {}'.format(g, prime % 8)) elif g == 3: if prime % 3 != 2: raise ValueError('bad g {}, mod3 {}'.format(g, prime % 3)) elif g == 4: pass elif g == 5: if prime % 5 not in (1, 4): raise ValueError('bad g {}, mod5 {}'.format(g, prime % 5)) elif g == 6: if prime % 24 not in (19, 23): raise ValueError('bad g {}, mod24 {}'.format(g, prime % 24)) elif g == 7: if prime % 7 not in (3, 5, 6): raise ValueError('bad g {}, mod7 {}'.format(g, prime % 7)) else: raise ValueError('bad g {}'.format(g)) prime_sub1_div2 = (prime - 1) // 2 if factorization.Factorization.factorize(prime_sub1_div2)[0] != 1: raise ValueError('(prime - 1) // 2 is not prime') # Else it's good def check_prime_and_good(prime_bytes: bytes, g: int): good_prime = bytes(( 0xC7, 0x1C, 0xAE, 0xB9, 0xC6, 0xB1, 0xC9, 0x04, 0x8E, 0x6C, 0x52, 0x2F, 0x70, 0xF1, 0x3F, 0x73, 0x98, 0x0D, 0x40, 0x23, 0x8E, 0x3E, 0x21, 0xC1, 0x49, 0x34, 0xD0, 0x37, 0x56, 0x3D, 0x93, 0x0F, 0x48, 0x19, 0x8A, 0x0A, 0xA7, 0xC1, 0x40, 0x58, 0x22, 0x94, 0x93, 0xD2, 0x25, 0x30, 0xF4, 0xDB, 0xFA, 0x33, 0x6F, 0x6E, 0x0A, 0xC9, 0x25, 0x13, 0x95, 0x43, 0xAE, 0xD4, 0x4C, 0xCE, 0x7C, 0x37, 0x20, 0xFD, 0x51, 0xF6, 0x94, 0x58, 0x70, 0x5A, 0xC6, 0x8C, 0xD4, 0xFE, 0x6B, 0x6B, 0x13, 0xAB, 0xDC, 0x97, 0x46, 0x51, 0x29, 0x69, 0x32, 0x84, 0x54, 0xF1, 0x8F, 0xAF, 0x8C, 0x59, 0x5F, 0x64, 0x24, 0x77, 0xFE, 0x96, 0xBB, 0x2A, 0x94, 0x1D, 0x5B, 0xCD, 0x1D, 0x4A, 0xC8, 0xCC, 0x49, 0x88, 0x07, 0x08, 0xFA, 0x9B, 0x37, 0x8E, 0x3C, 0x4F, 0x3A, 0x90, 0x60, 0xBE, 0xE6, 0x7C, 0xF9, 0xA4, 0xA4, 0xA6, 0x95, 0x81, 0x10, 0x51, 0x90, 0x7E, 0x16, 0x27, 0x53, 0xB5, 0x6B, 0x0F, 0x6B, 0x41, 0x0D, 0xBA, 0x74, 0xD8, 0xA8, 0x4B, 0x2A, 0x14, 0xB3, 0x14, 0x4E, 0x0E, 0xF1, 0x28, 0x47, 0x54, 0xFD, 0x17, 0xED, 0x95, 0x0D, 0x59, 0x65, 0xB4, 0xB9, 0xDD, 0x46, 0x58, 0x2D, 0xB1, 0x17, 0x8D, 0x16, 0x9C, 0x6B, 0xC4, 0x65, 0xB0, 0xD6, 0xFF, 0x9C, 0xA3, 0x92, 0x8F, 0xEF, 0x5B, 0x9A, 0xE4, 0xE4, 0x18, 0xFC, 0x15, 0xE8, 0x3E, 0xBE, 0xA0, 0xF8, 0x7F, 0xA9, 0xFF, 0x5E, 0xED, 0x70, 0x05, 0x0D, 0xED, 0x28, 0x49, 0xF4, 0x7B, 0xF9, 0x59, 0xD9, 0x56, 0x85, 0x0C, 0xE9, 0x29, 0x85, 0x1F, 0x0D, 0x81, 0x15, 0xF6, 0x35, 0xB1, 0x05, 0xEE, 0x2E, 0x4E, 0x15, 0xD0, 0x4B, 0x24, 0x54, 0xBF, 0x6F, 0x4F, 0xAD, 0xF0, 0x34, 0xB1, 0x04, 0x03, 0x11, 0x9C, 0xD8, 0xE3, 0xB9, 0x2F, 0xCC, 0x5B)) if good_prime == prime_bytes: if g in (3, 4, 5, 7): return # It's good check_prime_and_good_check(int.from_bytes(prime_bytes, 'big'), g) def is_good_large(number: int, p: int) -> bool: return number > 0 and p - number > 0 SIZE_FOR_HASH = 256 def num_bytes_for_hash(number: bytes) -> bytes: return bytes(SIZE_FOR_HASH - len(number)) + number def big_num_for_hash(g: int) -> bytes: return g.to_bytes(SIZE_FOR_HASH, 'big') def sha256(*p: bytes) -> bytes: hash = hashlib.sha256() for q in p: hash.update(q) return hash.digest() def is_good_mod_exp_first(modexp, prime) -> bool: diff = prime - modexp min_diff_bits_count = 2048 - 64 max_mod_exp_size = 256 if diff < 0 or \ diff.bit_length() < min_diff_bits_count or \ modexp.bit_length() < min_diff_bits_count or \ (modexp.bit_length() + 7) // 8 > max_mod_exp_size: return False return True def xor(a: bytes, b: bytes) -> bytes: return bytes(x ^ y for x, y in zip(a, b)) def pbkdf2sha512(password: bytes, salt: bytes, iterations: int): return hashlib.pbkdf2_hmac('sha512', password, salt, iterations) def compute_hash(algo: types.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow, password: str): hash1 = sha256(algo.salt1, password.encode('utf-8'), algo.salt1) hash2 = sha256(algo.salt2, hash1, algo.salt2) hash3 = pbkdf2sha512(hash2, algo.salt1, 100000) return sha256(algo.salt2, hash3, algo.salt2) def compute_digest(algo: types.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow, password: str): try: check_prime_and_good(algo.p, algo.g) except ValueError: raise ValueError('bad p/g in password') value = pow(algo.g, int.from_bytes(compute_hash(algo, password), 'big'), int.from_bytes(algo.p, 'big')) return big_num_for_hash(value) # https://github.com/telegramdesktop/tdesktop/blob/18b74b90451a7db2379a9d753c9cbaf8734b4d5d/Telegram/SourceFiles/core/core_cloud_password.cpp def compute_check(request: types.account.Password, password: str): algo = request.current_algo if not isinstance(algo, types.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow): raise ValueError('unsupported password algorithm {}' .format(algo.__class__.__name__)) pw_hash = compute_hash(algo, password) p = int.from_bytes(algo.p, 'big') g = algo.g B = int.from_bytes(request.srp_B, 'big') try: check_prime_and_good(algo.p, g) except ValueError: raise ValueError('bad p/g in password') if not is_good_large(B, p): raise ValueError('bad b in check') x = int.from_bytes(pw_hash, 'big') p_for_hash = num_bytes_for_hash(algo.p) g_for_hash = big_num_for_hash(g) b_for_hash = num_bytes_for_hash(request.srp_B) g_x = pow(g, x, p) k = int.from_bytes(sha256(p_for_hash, g_for_hash), 'big') kg_x = (k * g_x) % p def generate_and_check_random(): random_size = 256 while True: random = os.urandom(random_size) a = int.from_bytes(random, 'big') A = pow(g, a, p) if is_good_mod_exp_first(A, p): a_for_hash = big_num_for_hash(A) u = int.from_bytes(sha256(a_for_hash, b_for_hash), 'big') if u > 0: return (a, a_for_hash, u) a, a_for_hash, u = generate_and_check_random() g_b = (B - kg_x) % p if not is_good_mod_exp_first(g_b, p): raise ValueError('bad g_b') ux = u * x a_ux = a + ux S = pow(g_b, a_ux, p) K = sha256(big_num_for_hash(S)) M1 = sha256( xor(sha256(p_for_hash), sha256(g_for_hash)), sha256(algo.salt1), sha256(algo.salt2), a_for_hash, b_for_hash, K ) return types.InputCheckPasswordSRP( request.srp_id, bytes(a_for_hash), bytes(M1)) Telethon-1.39.0/telethon/requestiter.py000066400000000000000000000104421475566265000201570ustar00rootroot00000000000000import abc import asyncio import time from . import helpers class RequestIter(abc.ABC): """ Helper class to deal with requests that need offsets to iterate. It has some facilities, such as automatically sleeping a desired amount of time between requests if needed (but not more). Can be used synchronously if the event loop is not running and as an asynchronous iterator otherwise. `limit` is the total amount of items that the iterator should return. This is handled on this base class, and will be always ``>= 0``. `left` will be reset every time the iterator is used and will indicate the amount of items that should be emitted left, so that subclasses can be more efficient and fetch only as many items as they need. Iterators may be used with ``reversed``, and their `reverse` flag will be set to `True` if that's the case. Note that if this flag is set, `buffer` should be filled in reverse too. """ def __init__(self, client, limit, *, reverse=False, wait_time=None, **kwargs): self.client = client self.reverse = reverse self.wait_time = wait_time self.kwargs = kwargs self.limit = max(float('inf') if limit is None else limit, 0) self.left = self.limit self.buffer = None self.index = 0 self.total = None self.last_load = 0 async def _init(self, **kwargs): """ Called when asynchronous initialization is necessary. All keyword arguments passed to `__init__` will be forwarded here, and it's preferable to use named arguments in the subclasses without defaults to avoid forgetting or misspelling any of them. This method may ``raise StopAsyncIteration`` if it cannot continue. This method may actually fill the initial buffer if it needs to, and similarly to `_load_next_chunk`, ``return True`` to indicate that this is the last iteration (just the initial load). """ async def __anext__(self): if self.buffer is None: self.buffer = [] if await self._init(**self.kwargs): self.left = len(self.buffer) if self.left <= 0: # <= 0 because subclasses may change it raise StopAsyncIteration if self.index == len(self.buffer): # asyncio will handle times <= 0 to sleep 0 seconds if self.wait_time: await asyncio.sleep( self.wait_time - (time.time() - self.last_load) ) self.last_load = time.time() self.index = 0 self.buffer = [] if await self._load_next_chunk(): self.left = len(self.buffer) if not self.buffer: raise StopAsyncIteration result = self.buffer[self.index] self.left -= 1 self.index += 1 return result def __next__(self): try: return self.client.loop.run_until_complete(self.__anext__()) except StopAsyncIteration: raise StopIteration def __aiter__(self): self.buffer = None self.index = 0 self.last_load = 0 self.left = self.limit return self def __iter__(self): if self.client.loop.is_running(): raise RuntimeError( 'You must use "async for" if the event loop ' 'is running (i.e. you are inside an "async def")' ) return self.__aiter__() async def collect(self): """ Create a `self` iterator and collect it into a `TotalList` (a normal list with a `.total` attribute). """ result = helpers.TotalList() async for message in self: result.append(message) result.total = self.total return result @abc.abstractmethod async def _load_next_chunk(self): """ Called when the next chunk is necessary. It should extend the `buffer` with new items. It should return `True` if it's the last chunk, after which moment the method won't be called again during the same iteration. """ raise NotImplementedError def __reversed__(self): self.reverse = not self.reverse return self # __aiter__ will be called after, too Telethon-1.39.0/telethon/sessions/000077500000000000000000000000001475566265000170765ustar00rootroot00000000000000Telethon-1.39.0/telethon/sessions/__init__.py000066400000000000000000000002041475566265000212030ustar00rootroot00000000000000from .abstract import Session from .memory import MemorySession from .sqlite import SQLiteSession from .string import StringSession Telethon-1.39.0/telethon/sessions/abstract.py000066400000000000000000000117431475566265000212610ustar00rootroot00000000000000from abc import ABC, abstractmethod class Session(ABC): def __init__(self): pass def clone(self, to_instance=None): """ Creates a clone of this session file. """ return to_instance or self.__class__() @abstractmethod def set_dc(self, dc_id, server_address, port): """ Sets the information of the data center address and port that the library should connect to, as well as the data center ID, which is currently unused. """ raise NotImplementedError @property @abstractmethod def dc_id(self): """ Returns the currently-used data center ID. """ raise NotImplementedError @property @abstractmethod def server_address(self): """ Returns the server address where the library should connect to. """ raise NotImplementedError @property @abstractmethod def port(self): """ Returns the port to which the library should connect to. """ raise NotImplementedError @property @abstractmethod def auth_key(self): """ Returns an ``AuthKey`` instance associated with the saved data center, or `None` if a new one should be generated. """ raise NotImplementedError @auth_key.setter @abstractmethod def auth_key(self, value): """ Sets the ``AuthKey`` to be used for the saved data center. """ raise NotImplementedError @property @abstractmethod def takeout_id(self): """ Returns an ID of the takeout process initialized for this session, or `None` if there's no were any unfinished takeout requests. """ raise NotImplementedError @takeout_id.setter @abstractmethod def takeout_id(self, value): """ Sets the ID of the unfinished takeout process for this session. """ raise NotImplementedError @abstractmethod def get_update_state(self, entity_id): """ Returns the ``UpdateState`` associated with the given `entity_id`. If the `entity_id` is 0, it should return the ``UpdateState`` for no specific channel (the "general" state). If no state is known it should ``return None``. """ raise NotImplementedError @abstractmethod def set_update_state(self, entity_id, state): """ Sets the given ``UpdateState`` for the specified `entity_id`, which should be 0 if the ``UpdateState`` is the "general" state (and not for any specific channel). """ raise NotImplementedError @abstractmethod def get_update_states(self): """ Returns an iterable over all known pairs of ``(entity ID, update state)``. """ def close(self): """ Called on client disconnection. Should be used to free any used resources. Can be left empty if none. """ @abstractmethod def save(self): """ Called whenever important properties change. It should make persist the relevant session information to disk. """ raise NotImplementedError @abstractmethod def delete(self): """ Called upon client.log_out(). Should delete the stored information from disk since it's not valid anymore. """ raise NotImplementedError @classmethod def list_sessions(cls): """ Lists available sessions. Not used by the library itself. """ return [] @abstractmethod def process_entities(self, tlo): """ Processes the input ``TLObject`` or ``list`` and saves whatever information is relevant (e.g., ID or access hash). """ raise NotImplementedError @abstractmethod def get_input_entity(self, key): """ Turns the given key into an ``InputPeer`` (e.g. ``InputPeerUser``). The library uses this method whenever an ``InputPeer`` is needed to suit several purposes (e.g. user only provided its ID or wishes to use a cached username to avoid extra RPC). """ raise NotImplementedError @abstractmethod def cache_file(self, md5_digest, file_size, instance): """ Caches the given file information persistently, so that it doesn't need to be re-uploaded in case the file is used again. The ``instance`` will be either an ``InputPhoto`` or ``InputDocument``, both with an ``.id`` and ``.access_hash`` attributes. """ raise NotImplementedError @abstractmethod def get_file(self, md5_digest, file_size, cls): """ Returns an instance of ``cls`` if the ``md5_digest`` and ``file_size`` match an existing saved record. The class will either be an ``InputPhoto`` or ``InputDocument``, both with two parameters ``id`` and ``access_hash`` in that order. """ raise NotImplementedError Telethon-1.39.0/telethon/sessions/memory.py000066400000000000000000000202101475566265000207530ustar00rootroot00000000000000from enum import Enum from .abstract import Session from .. import utils from ..tl import TLObject from ..tl.types import ( PeerUser, PeerChat, PeerChannel, InputPeerUser, InputPeerChat, InputPeerChannel, InputPhoto, InputDocument ) class _SentFileType(Enum): DOCUMENT = 0 PHOTO = 1 @staticmethod def from_type(cls): if cls == InputDocument: return _SentFileType.DOCUMENT elif cls == InputPhoto: return _SentFileType.PHOTO else: raise ValueError('The cls must be either InputDocument/InputPhoto') class MemorySession(Session): def __init__(self): super().__init__() self._dc_id = 0 self._server_address = None self._port = None self._auth_key = None self._takeout_id = None self._files = {} self._entities = set() self._update_states = {} def set_dc(self, dc_id, server_address, port): self._dc_id = dc_id or 0 self._server_address = server_address self._port = port @property def dc_id(self): return self._dc_id @property def server_address(self): return self._server_address @property def port(self): return self._port @property def auth_key(self): return self._auth_key @auth_key.setter def auth_key(self, value): self._auth_key = value @property def takeout_id(self): return self._takeout_id @takeout_id.setter def takeout_id(self, value): self._takeout_id = value def get_update_state(self, entity_id): return self._update_states.get(entity_id, None) def set_update_state(self, entity_id, state): self._update_states[entity_id] = state def get_update_states(self): return self._update_states.items() def close(self): pass def save(self): pass def delete(self): pass @staticmethod def _entity_values_to_row(id, hash, username, phone, name): # While this is a simple implementation it might be overrode by, # other classes so they don't need to implement the plural form # of the method. Don't remove. return id, hash, username, phone, name def _entity_to_row(self, e): if not isinstance(e, TLObject): return try: p = utils.get_input_peer(e, allow_self=False) marked_id = utils.get_peer_id(p) except TypeError: # Note: `get_input_peer` already checks for non-zero `access_hash`. # See issues #354 and #392. It also checks that the entity # is not `min`, because its `access_hash` cannot be used # anywhere (since layer 102, there are two access hashes). return if isinstance(p, (InputPeerUser, InputPeerChannel)): p_hash = p.access_hash elif isinstance(p, InputPeerChat): p_hash = 0 else: return username = getattr(e, 'username', None) or None if username is not None: username = username.lower() phone = getattr(e, 'phone', None) name = utils.get_display_name(e) or None return self._entity_values_to_row( marked_id, p_hash, username, phone, name ) def _entities_to_rows(self, tlo): if not isinstance(tlo, TLObject) and utils.is_list_like(tlo): # This may be a list of users already for instance entities = tlo else: entities = [] if hasattr(tlo, 'user'): entities.append(tlo.user) if hasattr(tlo, 'chat'): entities.append(tlo.chat) if hasattr(tlo, 'chats') and utils.is_list_like(tlo.chats): entities.extend(tlo.chats) if hasattr(tlo, 'users') and utils.is_list_like(tlo.users): entities.extend(tlo.users) rows = [] # Rows to add (id, hash, username, phone, name) for e in entities: row = self._entity_to_row(e) if row: rows.append(row) return rows def process_entities(self, tlo): self._entities |= set(self._entities_to_rows(tlo)) def get_entity_rows_by_phone(self, phone): try: return next((id, hash) for id, hash, _, found_phone, _ in self._entities if found_phone == phone) except StopIteration: pass def get_entity_rows_by_username(self, username): try: return next((id, hash) for id, hash, found_username, _, _ in self._entities if found_username == username) except StopIteration: pass def get_entity_rows_by_name(self, name): try: return next((id, hash) for id, hash, _, _, found_name in self._entities if found_name == name) except StopIteration: pass def get_entity_rows_by_id(self, id, exact=True): try: if exact: return next((found_id, hash) for found_id, hash, _, _, _ in self._entities if found_id == id) else: ids = ( utils.get_peer_id(PeerUser(id)), utils.get_peer_id(PeerChat(id)), utils.get_peer_id(PeerChannel(id)) ) return next((found_id, hash) for found_id, hash, _, _, _ in self._entities if found_id in ids) except StopIteration: pass def get_input_entity(self, key): try: if key.SUBCLASS_OF_ID in (0xc91c90b6, 0xe669bf46, 0x40f202fd): # hex(crc32(b'InputPeer', b'InputUser' and b'InputChannel')) # We already have an Input version, so nothing else required return key # Try to early return if this key can be casted as input peer return utils.get_input_peer(key) except (AttributeError, TypeError): # Not a TLObject or can't be cast into InputPeer if isinstance(key, TLObject): key = utils.get_peer_id(key) exact = True else: exact = not isinstance(key, int) or key < 0 result = None if isinstance(key, str): phone = utils.parse_phone(key) if phone: result = self.get_entity_rows_by_phone(phone) else: username, invite = utils.parse_username(key) if username and not invite: result = self.get_entity_rows_by_username(username) else: tup = utils.resolve_invite_link(key)[1] if tup: result = self.get_entity_rows_by_id(tup, exact=False) elif isinstance(key, int): result = self.get_entity_rows_by_id(key, exact) if not result and isinstance(key, str): result = self.get_entity_rows_by_name(key) if result: entity_id, entity_hash = result # unpack resulting tuple entity_id, kind = utils.resolve_id(entity_id) # removes the mark and returns type of entity if kind == PeerUser: return InputPeerUser(entity_id, entity_hash) elif kind == PeerChat: return InputPeerChat(entity_id) elif kind == PeerChannel: return InputPeerChannel(entity_id, entity_hash) else: raise ValueError('Could not find input entity with key ', key) def cache_file(self, md5_digest, file_size, instance): if not isinstance(instance, (InputDocument, InputPhoto)): raise TypeError('Cannot cache %s instance' % type(instance)) key = (md5_digest, file_size, _SentFileType.from_type(type(instance))) value = (instance.id, instance.access_hash) self._files[key] = value def get_file(self, md5_digest, file_size, cls): key = (md5_digest, file_size, _SentFileType.from_type(cls)) try: return cls(*self._files[key]) except KeyError: return None Telethon-1.39.0/telethon/sessions/sqlite.py000066400000000000000000000304371475566265000207600ustar00rootroot00000000000000import datetime import os import time from ..tl import types from .memory import MemorySession, _SentFileType from .. import utils from ..crypto import AuthKey from ..tl.types import ( InputPhoto, InputDocument, PeerUser, PeerChat, PeerChannel ) try: import sqlite3 sqlite3_err = None except ImportError as e: sqlite3 = None sqlite3_err = type(e) EXTENSION = '.session' CURRENT_VERSION = 7 # database version class SQLiteSession(MemorySession): """This session contains the required information to login into your Telegram account. NEVER give the saved session file to anyone, since they would gain instant access to all your messages and contacts. If you think the session has been compromised, close all the sessions through an official Telegram client to revoke the authorization. """ def __init__(self, session_id=None): if sqlite3 is None: raise sqlite3_err super().__init__() self.filename = ':memory:' self.save_entities = True if session_id: self.filename = session_id if not self.filename.endswith(EXTENSION): self.filename += EXTENSION self._conn = None c = self._cursor() c.execute("select name from sqlite_master " "where type='table' and name='version'") if c.fetchone(): # Tables already exist, check for the version c.execute("select version from version") version = c.fetchone()[0] if version < CURRENT_VERSION: self._upgrade_database(old=version) c.execute("delete from version") c.execute("insert into version values (?)", (CURRENT_VERSION,)) self.save() # These values will be saved c.execute('select * from sessions') tuple_ = c.fetchone() if tuple_: self._dc_id, self._server_address, self._port, key, \ self._takeout_id = tuple_ self._auth_key = AuthKey(data=key) c.close() else: # Tables don't exist, create new ones self._create_table( c, "version (version integer primary key)" , """sessions ( dc_id integer primary key, server_address text, port integer, auth_key blob, takeout_id integer )""" , """entities ( id integer primary key, hash integer not null, username text, phone integer, name text, date integer )""" , """sent_files ( md5_digest blob, file_size integer, type integer, id integer, hash integer, primary key(md5_digest, file_size, type) )""" , """update_state ( id integer primary key, pts integer, qts integer, date integer, seq integer )""" ) c.execute("insert into version values (?)", (CURRENT_VERSION,)) self._update_session_table() c.close() self.save() def clone(self, to_instance=None): cloned = super().clone(to_instance) cloned.save_entities = self.save_entities return cloned def _upgrade_database(self, old): c = self._cursor() if old == 1: old += 1 # old == 1 doesn't have the old sent_files so no need to drop if old == 2: old += 1 # Old cache from old sent_files lasts then a day anyway, drop c.execute('drop table sent_files') self._create_table(c, """sent_files ( md5_digest blob, file_size integer, type integer, id integer, hash integer, primary key(md5_digest, file_size, type) )""") if old == 3: old += 1 self._create_table(c, """update_state ( id integer primary key, pts integer, qts integer, date integer, seq integer )""") if old == 4: old += 1 c.execute("alter table sessions add column takeout_id integer") if old == 5: # Not really any schema upgrade, but potentially all access # hashes for User and Channel are wrong, so drop them off. old += 1 c.execute('delete from entities') if old == 6: old += 1 c.execute("alter table entities add column date integer") c.close() @staticmethod def _create_table(c, *definitions): for definition in definitions: c.execute('create table {}'.format(definition)) # Data from sessions should be kept as properties # not to fetch the database every time we need it def set_dc(self, dc_id, server_address, port): super().set_dc(dc_id, server_address, port) self._update_session_table() # Fetch the auth_key corresponding to this data center row = self._execute('select auth_key from sessions') if row and row[0]: self._auth_key = AuthKey(data=row[0]) else: self._auth_key = None @MemorySession.auth_key.setter def auth_key(self, value): self._auth_key = value self._update_session_table() @MemorySession.takeout_id.setter def takeout_id(self, value): self._takeout_id = value self._update_session_table() def _update_session_table(self): c = self._cursor() # While we can save multiple rows into the sessions table # currently we only want to keep ONE as the tables don't # tell us which auth_key's are usable and will work. Needs # some more work before being able to save auth_key's for # multiple DCs. Probably done differently. c.execute('delete from sessions') c.execute('insert or replace into sessions values (?,?,?,?,?)', ( self._dc_id, self._server_address, self._port, self._auth_key.key if self._auth_key else b'', self._takeout_id )) c.close() def get_update_state(self, entity_id): row = self._execute('select pts, qts, date, seq from update_state ' 'where id = ?', entity_id) if row: pts, qts, date, seq = row date = datetime.datetime.fromtimestamp( date, tz=datetime.timezone.utc) return types.updates.State(pts, qts, date, seq, unread_count=0) def set_update_state(self, entity_id, state): self._execute('insert or replace into update_state values (?,?,?,?,?)', entity_id, state.pts, state.qts, state.date.timestamp(), state.seq) def get_update_states(self): c = self._cursor() try: rows = c.execute('select id, pts, qts, date, seq from update_state').fetchall() return ((row[0], types.updates.State( pts=row[1], qts=row[2], date=datetime.datetime.fromtimestamp(row[3], tz=datetime.timezone.utc), seq=row[4], unread_count=0) ) for row in rows) finally: c.close() def save(self): """Saves the current session object as session_user_id.session""" # This is a no-op if there are no changes to commit, so there's # no need for us to keep track of an "unsaved changes" variable. if self._conn is not None: self._conn.commit() def _cursor(self): """Asserts that the connection is open and returns a cursor""" if self._conn is None: self._conn = sqlite3.connect(self.filename, check_same_thread=False) return self._conn.cursor() def _execute(self, stmt, *values): """ Gets a cursor, executes `stmt` and closes the cursor, fetching one row afterwards and returning its result. """ c = self._cursor() try: return c.execute(stmt, values).fetchone() finally: c.close() def close(self): """Closes the connection unless we're working in-memory""" if self.filename != ':memory:': if self._conn is not None: self._conn.commit() self._conn.close() self._conn = None def delete(self): """Deletes the current session file""" if self.filename == ':memory:': return True try: os.remove(self.filename) return True except OSError: return False @classmethod def list_sessions(cls): """Lists all the sessions of the users who have ever connected using this client and never logged out """ return [os.path.splitext(os.path.basename(f))[0] for f in os.listdir('.') if f.endswith(EXTENSION)] # Entity processing def process_entities(self, tlo): """ Processes all the found entities on the given TLObject, unless .save_entities is False. """ if not self.save_entities: return rows = self._entities_to_rows(tlo) if not rows: return c = self._cursor() try: now_tup = (int(time.time()),) rows = [row + now_tup for row in rows] c.executemany( 'insert or replace into entities values (?,?,?,?,?,?)', rows) finally: c.close() def get_entity_rows_by_phone(self, phone): return self._execute( 'select id, hash from entities where phone = ?', phone) def get_entity_rows_by_username(self, username): c = self._cursor() try: results = c.execute( 'select id, hash, date from entities where username = ?', (username,) ).fetchall() if not results: return None # If there is more than one result for the same username, evict the oldest one if len(results) > 1: results.sort(key=lambda t: t[2] or 0) c.executemany('update entities set username = null where id = ?', [(t[0],) for t in results[:-1]]) return results[-1][0], results[-1][1] finally: c.close() def get_entity_rows_by_name(self, name): return self._execute( 'select id, hash from entities where name = ?', name) def get_entity_rows_by_id(self, id, exact=True): if exact: return self._execute( 'select id, hash from entities where id = ?', id) else: return self._execute( 'select id, hash from entities where id in (?,?,?)', utils.get_peer_id(PeerUser(id)), utils.get_peer_id(PeerChat(id)), utils.get_peer_id(PeerChannel(id)) ) # File processing def get_file(self, md5_digest, file_size, cls): row = self._execute( 'select id, hash from sent_files ' 'where md5_digest = ? and file_size = ? and type = ?', md5_digest, file_size, _SentFileType.from_type(cls).value ) if row: # Both allowed classes have (id, access_hash) as parameters return cls(row[0], row[1]) def cache_file(self, md5_digest, file_size, instance): if not isinstance(instance, (InputDocument, InputPhoto)): raise TypeError('Cannot cache %s instance' % type(instance)) self._execute( 'insert or replace into sent_files values (?,?,?,?,?)', md5_digest, file_size, _SentFileType.from_type(type(instance)).value, instance.id, instance.access_hash ) Telethon-1.39.0/telethon/sessions/string.py000066400000000000000000000037061475566265000207640ustar00rootroot00000000000000import base64 import ipaddress import struct from .abstract import Session from .memory import MemorySession from ..crypto import AuthKey _STRUCT_PREFORMAT = '>B{}sH256s' CURRENT_VERSION = '1' class StringSession(MemorySession): """ This session file can be easily saved and loaded as a string. According to the initial design, it contains only the data that is necessary for successful connection and authentication, so takeout ID is not stored. It is thought to be used where you don't want to create any on-disk files but would still like to be able to save and load existing sessions by other means. You can use custom `encode` and `decode` functions, if present: * `encode` definition must be ``def encode(value: bytes) -> str:``. * `decode` definition must be ``def decode(value: str) -> bytes:``. """ def __init__(self, string: str = None): super().__init__() if string: if string[0] != CURRENT_VERSION: raise ValueError('Not a valid string') string = string[1:] ip_len = 4 if len(string) == 352 else 16 self._dc_id, ip, self._port, key = struct.unpack( _STRUCT_PREFORMAT.format(ip_len), StringSession.decode(string)) self._server_address = ipaddress.ip_address(ip).compressed if any(key): self._auth_key = AuthKey(key) @staticmethod def encode(x: bytes) -> str: return base64.urlsafe_b64encode(x).decode('ascii') @staticmethod def decode(x: str) -> bytes: return base64.urlsafe_b64decode(x) def save(self: Session): if not self.auth_key: return '' ip = ipaddress.ip_address(self.server_address).packed return CURRENT_VERSION + StringSession.encode(struct.pack( _STRUCT_PREFORMAT.format(len(ip)), self.dc_id, ip, self.port, self.auth_key.key )) Telethon-1.39.0/telethon/sync.py000066400000000000000000000050611475566265000165600ustar00rootroot00000000000000""" This magical module will rewrite all public methods in the public interface of the library so they can run the loop on their own if it's not already running. This rewrite may not be desirable if the end user always uses the methods they way they should be ran, but it's incredibly useful for quick scripts and the runtime overhead is relatively low. Some really common methods which are hardly used offer this ability by default, such as ``.start()`` and ``.run_until_disconnected()`` (since you may want to start, and then run until disconnected while using async event handlers). """ import asyncio import functools import inspect from . import events, errors, utils, connection, helpers from .client.account import _TakeoutClient from .client.telegramclient import TelegramClient from .tl import types, functions, custom from .tl.custom import ( Draft, Dialog, MessageButton, Forward, Button, Message, InlineResult, Conversation ) from .tl.custom.chatgetter import ChatGetter from .tl.custom.sendergetter import SenderGetter def _syncify_wrap(t, method_name): method = getattr(t, method_name) @functools.wraps(method) def syncified(*args, **kwargs): coro = method(*args, **kwargs) loop = helpers.get_running_loop() if loop.is_running(): return coro else: return loop.run_until_complete(coro) # Save an accessible reference to the original method setattr(syncified, '__tl.sync', method) setattr(t, method_name, syncified) def syncify(*types): """ Converts all the methods in the given types (class definitions) into synchronous, which return either the coroutine or the result based on whether ``asyncio's`` event loop is running. """ # Our asynchronous generators all are `RequestIter`, which already # provide a synchronous iterator variant, so we don't need to worry # about asyncgenfunction's here. for t in types: for name in dir(t): if not name.startswith('_') or name == '__call__': if inspect.iscoroutinefunction(getattr(t, name)): _syncify_wrap(t, name) syncify(TelegramClient, _TakeoutClient, Draft, Dialog, MessageButton, ChatGetter, SenderGetter, Forward, Message, InlineResult, Conversation) # Private special case, since a conversation's methods return # futures (but the public function themselves are synchronous). _syncify_wrap(Conversation, '_get_result') __all__ = [ 'TelegramClient', 'Button', 'types', 'functions', 'custom', 'errors', 'events', 'utils', 'connection' ] Telethon-1.39.0/telethon/tl/000077500000000000000000000000001475566265000156475ustar00rootroot00000000000000Telethon-1.39.0/telethon/tl/__init__.py000066400000000000000000000000521475566265000177550ustar00rootroot00000000000000from .tlobject import TLObject, TLRequest Telethon-1.39.0/telethon/tl/core/000077500000000000000000000000001475566265000165775ustar00rootroot00000000000000Telethon-1.39.0/telethon/tl/core/__init__.py000066400000000000000000000021201475566265000207030ustar00rootroot00000000000000""" This module holds core "special" types, which are more convenient ways to do stuff in a `telethon.network.mtprotosender.MTProtoSender` instance. Only special cases are gzip-packed data, the response message (not a client message), the message container which references these messages and would otherwise conflict with the rest, and finally the RpcResult: rpc_result#f35c6d01 req_msg_id:long result:bytes = RpcResult; Three things to note with this definition: 1. The constructor ID is actually ``42d36c2c``. 2. Those bytes are not read like the rest of bytes (length + payload). They are actually the raw bytes of another object, which can't be read directly because it depends on per-request information (since some can return ``Vector`` and ``Vector``). 3. Those bytes may be gzipped data, which needs to be treated early. """ from .tlmessage import TLMessage from .gzippacked import GzipPacked from .messagecontainer import MessageContainer from .rpcresult import RpcResult core_objects = {x.CONSTRUCTOR_ID: x for x in ( GzipPacked, MessageContainer, RpcResult )} Telethon-1.39.0/telethon/tl/core/gzippacked.py000066400000000000000000000024441475566265000212760ustar00rootroot00000000000000import gzip import struct from .. import TLObject class GzipPacked(TLObject): CONSTRUCTOR_ID = 0x3072cfa1 def __init__(self, data): self.data = data @staticmethod def gzip_if_smaller(content_related, data): """Calls bytes(request), and based on a certain threshold, optionally gzips the resulting data. If the gzipped data is smaller than the original byte array, this is returned instead. Note that this only applies to content related requests. """ if content_related and len(data) > 512: gzipped = bytes(GzipPacked(data)) return gzipped if len(gzipped) < len(data) else data else: return data def __bytes__(self): return struct.pack('`. """ return isinstance(self.original.action, types.ChannelAdminLogEventActionEditMessage) @property def deleted_message(self): """ Whether a message in this channel was deleted or not. If `True`, `old` will be present as `Message `. """ return isinstance(self.original.action, types.ChannelAdminLogEventActionDeleteMessage) @property def changed_admin(self): """ Whether the permissions for an admin in this channel changed or not. If `True`, `old` and `new` will be present as :tl:`ChannelParticipant`. """ return isinstance( self.original.action, types.ChannelAdminLogEventActionParticipantToggleAdmin) @property def changed_restrictions(self): """ Whether a message in this channel was edited or not. If `True`, `old` and `new` will be present as :tl:`ChannelParticipant`. """ return isinstance( self.original.action, types.ChannelAdminLogEventActionParticipantToggleBan) @property def changed_invites(self): """ Whether the invites in the channel were toggled or not. If `True`, `old` and `new` will be present as `bool`. """ return isinstance(self.original.action, types.ChannelAdminLogEventActionToggleInvites) @property def changed_location(self): """ Whether the location setting of the channel has changed or not. If `True`, `old` and `new` will be present as :tl:`ChannelLocation`. """ return isinstance(self.original.action, types.ChannelAdminLogEventActionChangeLocation) @property def joined(self): """ Whether `user` joined through the channel's public username or not. """ return isinstance(self.original.action, types.ChannelAdminLogEventActionParticipantJoin) @property def joined_invite(self): """ Whether a new user joined through an invite link to the channel or not. If `True`, `new` will be present as :tl:`ChannelParticipant`. """ return isinstance(self.original.action, types.ChannelAdminLogEventActionParticipantInvite) @property def left(self): """ Whether `user` left the channel or not. """ return isinstance(self.original.action, types.ChannelAdminLogEventActionParticipantLeave) @property def changed_hide_history(self): """ Whether hiding the previous message history for new members in the channel was toggled or not. If `True`, `old` and `new` will be present as `bool`. """ return isinstance(self.original.action, types.ChannelAdminLogEventActionTogglePreHistoryHidden) @property def changed_signatures(self): """ Whether the message signatures in the channel were toggled or not. If `True`, `old` and `new` will be present as `bool`. """ return isinstance(self.original.action, types.ChannelAdminLogEventActionToggleSignatures) @property def changed_pin(self): """ Whether a new message in this channel was pinned or not. If `True`, `new` will be present as `Message `. """ return isinstance(self.original.action, types.ChannelAdminLogEventActionUpdatePinned) @property def changed_default_banned_rights(self): """ Whether the default banned rights were changed or not. If `True`, `old` and `new` will be present as :tl:`ChatBannedRights`. """ return isinstance(self.original.action, types.ChannelAdminLogEventActionDefaultBannedRights) @property def stopped_poll(self): """ Whether a poll was stopped or not. If `True`, `new` will be present as `Message `. """ return isinstance(self.original.action, types.ChannelAdminLogEventActionStopPoll) @property def started_group_call(self): """ Whether a group call was started or not. If `True`, `new` will be present as :tl:`InputGroupCall`. """ return isinstance(self.original.action, types.ChannelAdminLogEventActionStartGroupCall) @property def discarded_group_call(self): """ Whether a group call was started or not. If `True`, `old` will be present as :tl:`InputGroupCall`. """ return isinstance(self.original.action, types.ChannelAdminLogEventActionDiscardGroupCall) @property def user_muted(self): """ Whether a participant was muted in the ongoing group call or not. If `True`, `new` will be present as :tl:`GroupCallParticipant`. """ return isinstance(self.original.action, types.ChannelAdminLogEventActionParticipantMute) @property def user_unmutted(self): """ Whether a participant was unmuted from the ongoing group call or not. If `True`, `new` will be present as :tl:`GroupCallParticipant`. """ return isinstance(self.original.action, types.ChannelAdminLogEventActionParticipantUnmute) @property def changed_call_settings(self): """ Whether the group call settings were changed or not. If `True`, `new` will be `True` if new users are muted on join. """ return isinstance(self.original.action, types.ChannelAdminLogEventActionToggleGroupCallSetting) @property def changed_history_ttl(self): """ Whether the Time To Live of the message history has changed. Messages sent after this change will have a ``ttl_period`` in seconds indicating how long they should live for before being auto-deleted. If `True`, `old` will be the old TTL, and `new` the new TTL, in seconds. """ return isinstance(self.original.action, types.ChannelAdminLogEventActionChangeHistoryTTL) @property def deleted_exported_invite(self): """ Whether the exported chat invite has been deleted. If `True`, `old` will be the deleted :tl:`ExportedChatInvite`. """ return isinstance(self.original.action, types.ChannelAdminLogEventActionExportedInviteDelete) @property def edited_exported_invite(self): """ Whether the exported chat invite has been edited. If `True`, `old` and `new` will be the old and new :tl:`ExportedChatInvite`, respectively. """ return isinstance(self.original.action, types.ChannelAdminLogEventActionExportedInviteEdit) @property def revoked_exported_invite(self): """ Whether the exported chat invite has been revoked. If `True`, `old` will be the revoked :tl:`ExportedChatInvite`. """ return isinstance(self.original.action, types.ChannelAdminLogEventActionExportedInviteRevoke) @property def joined_by_invite(self): """ Whether a new participant has joined with the use of an invite link. If `True`, `old` will be pre-existing (old) :tl:`ExportedChatInvite` used to join. """ return isinstance(self.original.action, types.ChannelAdminLogEventActionParticipantJoinByInvite) @property def changed_user_volume(self): """ Whether a participant's volume in a call has been changed. If `True`, `new` will be the updated :tl:`GroupCallParticipant`. """ return isinstance(self.original.action, types.ChannelAdminLogEventActionParticipantVolume) def __str__(self): return str(self.original) def stringify(self): return self.original.stringify() Telethon-1.39.0/telethon/tl/custom/button.py000066400000000000000000000301731475566265000210520ustar00rootroot00000000000000from .. import types from ... import utils class Button: """ .. note:: This class is used to **define** reply markups, e.g. when sending a message or replying to events. When you access `Message.buttons ` they are actually `MessageButton `, so you might want to refer to that class instead. Helper class to allow defining ``reply_markup`` when sending a message with inline or keyboard buttons. You should make use of the defined class methods to create button instances instead making them yourself (i.e. don't do ``Button(...)`` but instead use methods line `Button.inline(...) ` etc. You can use `inline`, `switch_inline`, `url`, `auth`, `buy` and `game` together to create inline buttons (under the message). You can use `text`, `request_location`, `request_phone` and `request_poll` together to create a reply markup (replaces the user keyboard). You can also configure the aspect of the reply with these. The latest message with a reply markup will be the one shown to the user (messages contain the buttons, not the chat itself). You **cannot** mix the two type of buttons together, and it will error if you try to do so. The text for all buttons may be at most 142 characters. If more characters are given, Telegram will cut the text to 128 characters and add the ellipsis (…) character as the 129. """ def __init__(self, button, *, resize, single_use, selective): self.button = button self.resize = resize self.single_use = single_use self.selective = selective @staticmethod def _is_inline(button): """ Returns `True` if the button belongs to an inline keyboard. """ return isinstance(button, ( types.KeyboardButtonBuy, types.KeyboardButtonCallback, types.KeyboardButtonGame, types.KeyboardButtonSwitchInline, types.KeyboardButtonUrl, types.InputKeyboardButtonUrlAuth, types.KeyboardButtonWebView, )) @staticmethod def inline(text, data=None): """ Creates a new inline button with some payload data in it. If `data` is omitted, the given `text` will be used as `data`. In any case `data` should be either `bytes` or `str`. Note that the given `data` must be less or equal to 64 bytes. If more than 64 bytes are passed as data, ``ValueError`` is raised. If you need to store more than 64 bytes, consider saving the real data in a database and a reference to that data inside the button. When the user clicks this button, `events.CallbackQuery ` will trigger with the same data that the button contained, so that you can determine which button was pressed. """ if not data: data = text.encode('utf-8') elif not isinstance(data, (bytes, bytearray, memoryview)): data = str(data).encode('utf-8') if len(data) > 64: raise ValueError('Too many bytes for the data') return types.KeyboardButtonCallback(text, data) @staticmethod def switch_inline(text, query='', same_peer=False): """ Creates a new inline button to switch to inline query. If `query` is given, it will be the default text to be used when making the inline query. If ``same_peer is True`` the inline query will directly be set under the currently opened chat. Otherwise, the user will have to select a different dialog to make the query. When the user clicks this button, after a chat is selected, their input field will be filled with the username of your bot followed by the query text, ready to make inline queries. """ return types.KeyboardButtonSwitchInline(text, query, same_peer) @staticmethod def url(text, url=None): """ Creates a new inline button to open the desired URL on click. If no `url` is given, the `text` will be used as said URL instead. You cannot detect that the user clicked this button directly. When the user clicks this button, a confirmation box will be shown to the user asking whether they want to open the displayed URL unless the domain is trusted, and once confirmed the URL will open in their device. """ return types.KeyboardButtonUrl(text, url or text) @staticmethod def auth(text, url=None, *, bot=None, write_access=False, fwd_text=None): """ Creates a new inline button to authorize the user at the given URL. You should set the `url` to be on the same domain as the one configured for the desired `bot` via `@BotFather `_ using the ``/setdomain`` command. For more information about letting the user login via Telegram to a certain domain, see https://core.telegram.org/widgets/login. If no `url` is specified, it will default to `text`. Args: bot (`hints.EntityLike`): The bot that requires this authorization. By default, this is the bot that is currently logged in (itself), although you may pass a different input peer. .. note:: For now, you cannot use ID or username for this argument. If you want to use a different bot than the one currently logged in, you must manually use `client.get_input_entity() `. write_access (`bool`): Whether write access is required or not. This is `False` by default (read-only access). fwd_text (`str`): The new text to show in the button if the message is forwarded. By default, the button text will be the same. When the user clicks this button, a confirmation box will be shown to the user asking whether they want to login to the specified domain. """ return types.InputKeyboardButtonUrlAuth( text=text, url=url or text, bot=utils.get_input_user(bot or types.InputUserSelf()), request_write_access=write_access, fwd_text=fwd_text ) @classmethod def text(cls, text, *, resize=None, single_use=None, selective=None): """ Creates a new keyboard button with the given text. Args: resize (`bool`): If present, the entire keyboard will be reconfigured to be resized and be smaller if there are not many buttons. single_use (`bool`): If present, the entire keyboard will be reconfigured to be usable only once before it hides itself. selective (`bool`): If present, the entire keyboard will be reconfigured to be "selective". The keyboard will be shown only to specific users. It will target users that are @mentioned in the text of the message or to the sender of the message you reply to. When the user clicks this button, a text message with the same text as the button will be sent, and can be handled with `events.NewMessage `. You cannot distinguish between a button press and the user typing and sending exactly the same text on their own. """ return cls(types.KeyboardButton(text), resize=resize, single_use=single_use, selective=selective) @classmethod def request_location(cls, text, *, resize=None, single_use=None, selective=None): """ Creates a new keyboard button to request the user's location on click. ``resize``, ``single_use`` and ``selective`` are documented in `text`. When the user clicks this button, a confirmation box will be shown to the user asking whether they want to share their location with the bot, and if confirmed a message with geo media will be sent. """ return cls(types.KeyboardButtonRequestGeoLocation(text), resize=resize, single_use=single_use, selective=selective) @classmethod def request_phone(cls, text, *, resize=None, single_use=None, selective=None): """ Creates a new keyboard button to request the user's phone on click. ``resize``, ``single_use`` and ``selective`` are documented in `text`. When the user clicks this button, a confirmation box will be shown to the user asking whether they want to share their phone with the bot, and if confirmed a message with contact media will be sent. """ return cls(types.KeyboardButtonRequestPhone(text), resize=resize, single_use=single_use, selective=selective) @classmethod def request_poll(cls, text, *, force_quiz=False, resize=None, single_use=None, selective=None): """ Creates a new keyboard button to request the user to create a poll. If `force_quiz` is `False`, the user will be allowed to choose whether they want their poll to be a quiz or not. Otherwise, the user will be forced to create a quiz when creating the poll. If a poll is a quiz, there will be only one answer that is valid, and the votes cannot be retracted. Otherwise, users can vote and retract the vote, and the pol might be multiple choice. ``resize``, ``single_use`` and ``selective`` are documented in `text`. When the user clicks this button, a screen letting the user create a poll will be shown, and if they do create one, the poll will be sent. """ return cls(types.KeyboardButtonRequestPoll(text, quiz=force_quiz), resize=resize, single_use=single_use, selective=selective) @staticmethod def clear(selective=None): """ Clears all keyboard buttons after sending a message with this markup. When used, no other button should be present or it will be ignored. ``selective`` is as documented in `text`. """ return types.ReplyKeyboardHide(selective=selective) @staticmethod def force_reply(single_use=None, selective=None, placeholder=None): """ Forces a reply to the message with this markup. If used, no other button should be present or it will be ignored. ``single_use`` and ``selective`` are as documented in `text`. Args: placeholder (str): text to show the user at typing place of message. If the placeholder is too long, Telegram applications will crop the text (for example, to 64 characters and adding an ellipsis (…) character as the 65th). """ return types.ReplyKeyboardForceReply( single_use=single_use, selective=selective, placeholder=placeholder) @staticmethod def buy(text): """ Creates a new inline button to buy a product. This can only be used when sending files of type :tl:`InputMediaInvoice`, and must be the first button. If the button is not specified, Telegram will automatically add the button to the message. See the `Payments API `__ documentation for more information. """ return types.KeyboardButtonBuy(text) @staticmethod def game(text): """ Creates a new inline button to start playing a game. This should be used when sending files of type :tl:`InputMediaGame`, and must be the first button. See the `Games `__ documentation for more information on using games. """ return types.KeyboardButtonGame(text) Telethon-1.39.0/telethon/tl/custom/chatgetter.py000066400000000000000000000122341475566265000216670ustar00rootroot00000000000000import abc from ... import errors, utils from ...tl import types class ChatGetter(abc.ABC): """ Helper base class that introduces the `chat`, `input_chat` and `chat_id` properties and `get_chat` and `get_input_chat` methods. """ def __init__(self, chat_peer=None, *, input_chat=None, chat=None, broadcast=None): self._chat_peer = chat_peer self._input_chat = input_chat self._chat = chat self._broadcast = broadcast self._client = None @property def chat(self): """ Returns the :tl:`User`, :tl:`Chat` or :tl:`Channel` where this object belongs to. It may be `None` if Telegram didn't send the chat. If you only need the ID, use `chat_id` instead. If you need to call a method which needs this chat, use `input_chat` instead. If you're using `telethon.events`, use `get_chat()` instead. """ return self._chat async def get_chat(self): """ Returns `chat`, but will make an API call to find the chat unless it's already cached. If you only need the ID, use `chat_id` instead. If you need to call a method which needs this chat, use `get_input_chat()` instead. """ # See `get_sender` for information about 'min'. if (self._chat is None or getattr(self._chat, 'min', None))\ and await self.get_input_chat(): try: self._chat =\ await self._client.get_entity(self._input_chat) except ValueError: await self._refetch_chat() return self._chat @property def input_chat(self): """ This :tl:`InputPeer` is the input version of the chat where the message was sent. Similarly to `input_sender `, this doesn't have things like username or similar, but still useful in some cases. Note that this might not be available if the library doesn't have enough information available. """ if self._input_chat is None and self._chat_peer and self._client: try: self._input_chat = self._client._mb_entity_cache.get( utils.get_peer_id(self._chat_peer, add_mark=False))._as_input_peer() except AttributeError: pass return self._input_chat async def get_input_chat(self): """ Returns `input_chat`, but will make an API call to find the input chat unless it's already cached. """ if self.input_chat is None and self.chat_id and self._client: try: # The chat may be recent, look in dialogs target = self.chat_id async for d in self._client.iter_dialogs(100): if d.id == target: self._chat = d.entity self._input_chat = d.input_entity break except errors.RPCError: pass return self._input_chat @property def chat_id(self): """ Returns the marked chat integer ID. Note that this value **will be different** from ``peer_id`` for incoming private messages, since the chat *to* which the messages go is to your own person, but the *chat* itself is with the one who sent the message. TL;DR; this gets the ID that you expect. If there is a chat in the object, `chat_id` will *always* be set, which is why you should use it instead of `chat.id `. """ return utils.get_peer_id(self._chat_peer) if self._chat_peer else None @property def is_private(self): """ `True` if the message was sent as a private message. Returns `None` if there isn't enough information (e.g. on `events.MessageDeleted `). """ return isinstance(self._chat_peer, types.PeerUser) if self._chat_peer else None @property def is_group(self): """ True if the message was sent on a group or megagroup. Returns `None` if there isn't enough information (e.g. on `events.MessageDeleted `). """ # TODO Cache could tell us more in the future if self._broadcast is None and hasattr(self.chat, 'broadcast'): self._broadcast = bool(self.chat.broadcast) if isinstance(self._chat_peer, types.PeerChannel): if self._broadcast is None: return None else: return not self._broadcast return isinstance(self._chat_peer, types.PeerChat) @property def is_channel(self): """`True` if the message was sent on a megagroup or channel.""" # The only case where chat peer could be none is in MessageDeleted, # however those always have the peer in channels. return isinstance(self._chat_peer, types.PeerChannel) async def _refetch_chat(self): """ Re-fetches chat information through other means. """ Telethon-1.39.0/telethon/tl/custom/conversation.py000066400000000000000000000457131475566265000222570ustar00rootroot00000000000000import asyncio import functools import inspect import itertools import time from .chatgetter import ChatGetter from ... import helpers, utils, errors # Sometimes the edits arrive very fast (within the same second). # In that case we add a small delta so that the age is older, for # comparision purposes. This value is enough for up to 1000 messages. _EDIT_COLLISION_DELTA = 0.001 def _checks_cancelled(f): @functools.wraps(f) def wrapper(self, *args, **kwargs): if self._cancelled: raise asyncio.CancelledError('The conversation was cancelled before') return f(self, *args, **kwargs) return wrapper class Conversation(ChatGetter): """ Represents a conversation inside an specific chat. A conversation keeps track of new messages since it was created until its exit and easily lets you query the current state. If you need a conversation across two or more chats, you should use two conversations and synchronize them as you better see fit. """ _id_counter = 0 _custom_counter = 0 def __init__(self, client, input_chat, *, timeout, total_timeout, max_messages, exclusive, replies_are_responses): # This call resets the client ChatGetter.__init__(self, input_chat=input_chat) self._id = Conversation._id_counter Conversation._id_counter += 1 self._client = client self._timeout = timeout self._total_timeout = total_timeout self._total_due = None self._outgoing = set() self._last_outgoing = 0 self._incoming = [] self._last_incoming = 0 self._max_incoming = max_messages self._last_read = None self._custom = {} self._pending_responses = {} self._pending_replies = {} self._pending_edits = {} self._pending_reads = {} self._exclusive = exclusive self._cancelled = False # The user is able to expect two responses for the same message. # {desired message ID: next incoming index} self._response_indices = {} if replies_are_responses: self._reply_indices = self._response_indices else: self._reply_indices = {} self._edit_dates = {} @_checks_cancelled async def send_message(self, *args, **kwargs): """ Sends a message in the context of this conversation. Shorthand for `telethon.client.messages.MessageMethods.send_message` with ``entity`` already set. """ sent = await self._client.send_message( self._input_chat, *args, **kwargs) # Albums will be lists, so handle that ms = sent if isinstance(sent, list) else (sent,) self._outgoing.update(m.id for m in ms) self._last_outgoing = ms[-1].id return sent @_checks_cancelled async def send_file(self, *args, **kwargs): """ Sends a file in the context of this conversation. Shorthand for `telethon.client.uploads.UploadMethods.send_file` with ``entity`` already set. """ sent = await self._client.send_file( self._input_chat, *args, **kwargs) # Albums will be lists, so handle that ms = sent if isinstance(sent, list) else (sent,) self._outgoing.update(m.id for m in ms) self._last_outgoing = ms[-1].id return sent @_checks_cancelled def mark_read(self, message=None): """ Marks as read the latest received message if ``message is None``. Otherwise, marks as read until the given message (or message ID). This is equivalent to calling `client.send_read_acknowledge `. """ if message is None: if self._incoming: message = self._incoming[-1].id else: message = 0 elif not isinstance(message, int): message = message.id return self._client.send_read_acknowledge( self._input_chat, max_id=message) def get_response(self, message=None, *, timeout=None): """ Gets the next message that responds to a previous one. This is the method you need most of the time, along with `get_edit`. Args: message (`Message ` | `int`, optional): The message (or the message ID) for which a response is expected. By default this is the last sent message. timeout (`int` | `float`, optional): If present, this `timeout` (in seconds) will override the per-action timeout defined for the conversation. .. code-block:: python async with client.conversation(...) as conv: await conv.send_message('Hey, what is your name?') response = await conv.get_response() name = response.text await conv.send_message('Nice to meet you, {}!'.format(name)) """ return self._get_message( message, self._response_indices, self._pending_responses, timeout, lambda x, y: True ) def get_reply(self, message=None, *, timeout=None): """ Gets the next message that explicitly replies to a previous one. """ return self._get_message( message, self._reply_indices, self._pending_replies, timeout, lambda x, y: x.reply_to and x.reply_to.reply_to_msg_id == y ) def _get_message( self, target_message, indices, pending, timeout, condition): """ Gets the next desired message under the desired condition. Args: target_message (`object`): The target message for which we want to find another response that applies based on `condition`. indices (`dict`): This dictionary remembers the last ID chosen for the input `target_message`. pending (`dict`): This dictionary remembers {msg_id: Future} to be set once `condition` is met. timeout (`int`): The timeout (in seconds) override to use for this operation. condition (`callable`): The condition callable that checks if an incoming message is a valid response. """ start_time = time.time() target_id = self._get_message_id(target_message) # If there is no last-chosen ID, make sure to pick one *after* # the input message, since we don't want responses back in time if target_id not in indices: for i, incoming in enumerate(self._incoming): if incoming.id > target_id: indices[target_id] = i break else: indices[target_id] = len(self._incoming) # We will always return a future from here, even if the result # can be set immediately. Otherwise, needing to await only # sometimes is an annoying edge case (i.e. we would return # a `Message` but `get_response()` always `await`'s). future = self._client.loop.create_future() # If there are enough responses saved return the next one last_idx = indices[target_id] if last_idx < len(self._incoming): incoming = self._incoming[last_idx] if condition(incoming, target_id): indices[target_id] += 1 future.set_result(incoming) return future # Otherwise the next incoming response will be the one to use # # Note how we fill "pending" before giving control back to the # event loop through "await". We want to register it as soon as # possible, since any other task switch may arrive with the result. pending[target_id] = future return self._get_result(future, start_time, timeout, pending, target_id) def get_edit(self, message=None, *, timeout=None): """ Awaits for an edit after the last message to arrive. The arguments are the same as those for `get_response`. """ start_time = time.time() target_id = self._get_message_id(message) target_date = self._edit_dates.get(target_id, 0) earliest_edit = min( (x for x in self._incoming if x.edit_date and x.id > target_id and x.edit_date.timestamp() > target_date ), key=lambda x: x.edit_date.timestamp(), default=None ) future = self._client.loop.create_future() if earliest_edit and earliest_edit.edit_date.timestamp() > target_date: self._edit_dates[target_id] = earliest_edit.edit_date.timestamp() future.set_result(earliest_edit) return future # we should always return something we can await # Otherwise the next incoming response will be the one to use self._pending_edits[target_id] = future return self._get_result(future, start_time, timeout, self._pending_edits, target_id) def wait_read(self, message=None, *, timeout=None): """ Awaits for the sent message to be marked as read. Note that receiving a response doesn't imply the message was read, and this action will also trigger even without a response. """ start_time = time.time() future = self._client.loop.create_future() target_id = self._get_message_id(message) if self._last_read is None: self._last_read = target_id - 1 if self._last_read >= target_id: return self._pending_reads[target_id] = future return self._get_result(future, start_time, timeout, self._pending_reads, target_id) async def wait_event(self, event, *, timeout=None): """ Waits for a custom event to occur. Timeouts still apply. .. note:: **Only use this if there isn't another method available!** For example, don't use `wait_event` for new messages, since `get_response` already exists, etc. Unless you're certain that your code will run fast enough, generally you should get a "handle" of this special coroutine before acting. In this example you will see how to wait for a user to join a group with proper use of `wait_event`: .. code-block:: python from telethon import TelegramClient, events client = TelegramClient(...) group_id = ... async def main(): # Could also get the user id from an event; this is just an example user_id = ... async with client.conversation(user_id) as conv: # Get a handle to the future event we'll wait for handle = conv.wait_event(events.ChatAction( group_id, func=lambda e: e.user_joined and e.user_id == user_id )) # Perform whatever action in between await conv.send_message('Please join this group before speaking to me!') # Wait for the event we registered above to fire event = await handle # Continue with the conversation await conv.send_message('Thanks!') This way your event can be registered before acting, since the response may arrive before your event was registered. It depends on your use case since this also means the event can arrive before you send a previous action. """ start_time = time.time() if isinstance(event, type): event = event() await event.resolve(self._client) counter = Conversation._custom_counter Conversation._custom_counter += 1 future = self._client.loop.create_future() self._custom[counter] = (event, future) try: return await self._get_result(future, start_time, timeout, self._custom, counter) finally: # Need to remove it from the dict if it times out, else we may # try and fail to set the result later (#1618). self._custom.pop(counter, None) async def _check_custom(self, built): for key, (ev, fut) in list(self._custom.items()): ev_type = type(ev) inst = built[ev_type] if inst: filter = ev.filter(inst) if inspect.isawaitable(filter): filter = await filter if filter: fut.set_result(inst) del self._custom[key] def _on_new_message(self, response): response = response.message if response.chat_id != self.chat_id or response.out: return if len(self._incoming) == self._max_incoming: self._cancel_all(ValueError('Too many incoming messages')) return self._incoming.append(response) # Most of the time, these dictionaries will contain just one item # TODO In fact, why not make it be that way? Force one item only. # How often will people want to wait for two responses at # the same time? It's impossible, first one will arrive # and then another, so they can do that. for msg_id, future in list(self._pending_responses.items()): self._response_indices[msg_id] = len(self._incoming) future.set_result(response) del self._pending_responses[msg_id] for msg_id, future in list(self._pending_replies.items()): if response.reply_to and msg_id == response.reply_to.reply_to_msg_id: self._reply_indices[msg_id] = len(self._incoming) future.set_result(response) del self._pending_replies[msg_id] def _on_edit(self, message): message = message.message if message.chat_id != self.chat_id or message.out: return # We have to update our incoming messages with the new edit date for i, m in enumerate(self._incoming): if m.id == message.id: self._incoming[i] = message break for msg_id, future in list(self._pending_edits.items()): if msg_id < message.id: edit_ts = message.edit_date.timestamp() # We compare <= because edit_ts resolution is always to # seconds, but we may have increased _edit_dates before. # Since the dates are ever growing this is not a problem. if edit_ts <= self._edit_dates.get(msg_id, 0): self._edit_dates[msg_id] += _EDIT_COLLISION_DELTA else: self._edit_dates[msg_id] = message.edit_date.timestamp() future.set_result(message) del self._pending_edits[msg_id] def _on_read(self, event): if event.chat_id != self.chat_id or event.inbox: return self._last_read = event.max_id for msg_id, pending in list(self._pending_reads.items()): if msg_id >= self._last_read: pending.set_result(True) del self._pending_reads[msg_id] def _get_message_id(self, message): if message is not None: # 0 is valid but false-y, check for None return message if isinstance(message, int) else message.id elif self._last_outgoing: return self._last_outgoing else: raise ValueError('No message was sent previously') @_checks_cancelled def _get_result(self, future, start_time, timeout, pending, target_id): due = self._total_due if timeout is None: timeout = self._timeout if timeout is not None: due = min(due, start_time + timeout) # NOTE: We can't try/finally to pop from pending here because # the event loop needs to get back to us, but it might # dispatch another update before, and in that case a # response could be set twice. So responses must be # cleared when their futures are set to a result. return asyncio.wait_for( future, timeout=None if due == float('inf') else due - time.time() ) def _cancel_all(self, exception=None): self._cancelled = True for pending in itertools.chain( self._pending_responses.values(), self._pending_replies.values(), self._pending_edits.values()): if exception: pending.set_exception(exception) else: pending.cancel() for _, fut in self._custom.values(): if exception: fut.set_exception(exception) else: fut.cancel() async def __aenter__(self): self._input_chat = \ await self._client.get_input_entity(self._input_chat) self._chat_peer = utils.get_peer(self._input_chat) # Make sure we're the only conversation in this chat if it's exclusive chat_id = utils.get_peer_id(self._chat_peer) conv_set = self._client._conversations[chat_id] if self._exclusive and conv_set: raise errors.AlreadyInConversationError() conv_set.add(self) self._cancelled = False self._last_outgoing = 0 self._last_incoming = 0 for d in ( self._outgoing, self._incoming, self._pending_responses, self._pending_replies, self._pending_edits, self._response_indices, self._reply_indices, self._edit_dates, self._custom): d.clear() if self._total_timeout: self._total_due = time.time() + self._total_timeout else: self._total_due = float('inf') return self def cancel(self): """ Cancels the current conversation. Pending responses and subsequent calls to get a response will raise ``asyncio.CancelledError``. This method is synchronous and should not be awaited. """ self._cancel_all() async def cancel_all(self): """ Calls `cancel` on *all* conversations in this chat. Note that you should ``await`` this method, since it's meant to be used outside of a context manager, and it needs to resolve the chat. """ chat_id = await self._client.get_peer_id(self._input_chat) for conv in self._client._conversations[chat_id]: conv.cancel() async def __aexit__(self, exc_type, exc_val, exc_tb): chat_id = utils.get_peer_id(self._chat_peer) conv_set = self._client._conversations[chat_id] conv_set.discard(self) if not conv_set: del self._client._conversations[chat_id] self._cancel_all() __enter__ = helpers._sync_enter __exit__ = helpers._sync_exit Telethon-1.39.0/telethon/tl/custom/dialog.py000066400000000000000000000127761475566265000210070ustar00rootroot00000000000000from . import Draft from .. import TLObject, types, functions 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 :meth:`.get_dialogs()`. Args: dialog (:tl:`Dialog`): The original ``Dialog`` instance. pinned (`bool`): Whether this dialog is pinned to the top or not. folder_id (`folder_id`): The folder ID that this dialog belongs to. archived (`bool`): Whether this dialog is archived or not (``folder_id is None``). message (`Message `): The last message sent on this dialog. Note that this member will not be updated when new messages arrive, it's only set on creation of the instance. date (`datetime`): The date of the last message sent on this dialog. entity (`entity`): The entity that belongs to this dialog (user, chat or channel). input_entity (:tl:`InputPeer`): Input version of the entity. id (`int`): The marked ID of the entity, which is guaranteed to be unique. name (`str`): Display name for this dialog. For chats and channels this is their title, and for users it's "First-Name Last-Name". title (`str`): Alias for `name`. unread_count (`int`): How many messages are currently unread in this dialog. Note that this value won't update when new messages arrive. unread_mentions_count (`int`): How many mentions are currently unread in this dialog. Note that this value won't update when new messages arrive. draft (`Draft `): The draft object in this dialog. It will not be `None`, so you can call ``draft.set_message(...)``. is_user (`bool`): `True` if the `entity` is a :tl:`User`. is_group (`bool`): `True` if the `entity` is a :tl:`Chat` or a :tl:`Channel` megagroup. is_channel (`bool`): `True` if the `entity` is a :tl:`Channel`. """ def __init__(self, client, dialog, entities, message): # Both entities and messages being dicts {ID: item} self._client = client self.dialog = dialog self.pinned = bool(dialog.pinned) self.folder_id = dialog.folder_id self.archived = dialog.folder_id is not None self.message = message self.date = getattr(self.message, 'date', None) self.entity = entities[utils.get_peer_id(dialog.peer)] self.input_entity = utils.get_input_peer(self.entity) self.id = utils.get_peer_id(self.entity) # ^ May be InputPeerSelf() self.name = self.title = utils.get_display_name(self.entity) self.unread_count = dialog.unread_count self.unread_mentions_count = dialog.unread_mentions_count self.draft = Draft(client, self.entity, self.dialog.draft) self.is_user = isinstance(self.entity, types.User) self.is_group = ( isinstance(self.entity, (types.Chat, types.ChatForbidden)) or (isinstance(self.entity, types.Channel) and self.entity.megagroup) ) self.is_channel = isinstance(self.entity, types.Channel) async def send_message(self, *args, **kwargs): """ Sends a message to this dialog. This is just a wrapper around ``client.send_message(dialog.input_entity, *args, **kwargs)``. """ return await self._client.send_message( self.input_entity, *args, **kwargs) async def delete(self, revoke=False): """ Deletes the dialog from your dialog list. If you own the channel this won't destroy it, only delete it from the list. Shorthand for `telethon.client.dialogs.DialogMethods.delete_dialog` with ``entity`` already set. """ # Pass the entire entity so the method can determine whether # the `Chat` is deactivated (in which case we don't kick ourselves, # or it would raise `PEER_ID_INVALID`). await self._client.delete_dialog(self.entity, revoke=revoke) async def archive(self, folder=1): """ Archives (or un-archives) this dialog. Args: folder (`int`, optional): The folder to which the dialog should be archived to. If you want to "un-archive" it, use ``folder=0``. Returns: The :tl:`Updates` object that the request produces. Example: .. code-block:: python # Archiving dialog.archive() # Un-archiving dialog.archive(0) """ return await self._client(functions.folders.EditPeerFoldersRequest([ types.InputFolderPeer(self.input_entity, folder_id=folder) ])) def to_dict(self): return { '_': 'Dialog', 'name': self.name, 'date': self.date, 'draft': self.draft, 'message': self.message, 'entity': self.entity, } def __str__(self): return TLObject.pretty_format(self.to_dict()) def stringify(self): return TLObject.pretty_format(self.to_dict(), indent=0) Telethon-1.39.0/telethon/tl/custom/draft.py000066400000000000000000000135321475566265000206370ustar00rootroot00000000000000import datetime from .. import TLObject, types from ..functions.messages import SaveDraftRequest from ..types import DraftMessage from ...errors import RPCError from ...extensions import markdown from ...utils import get_input_peer, get_peer, get_peer_id class Draft: """ Custom class that encapsulates a draft on the Telegram servers, providing an abstraction to change the message conveniently. The library will return instances of this class when calling :meth:`get_drafts()`. Args: date (`datetime`): The date of the draft. link_preview (`bool`): Whether the link preview is enabled or not. reply_to_msg_id (`int`): The message ID that the draft will reply to. """ def __init__(self, client, entity, draft): self._client = client self._peer = get_peer(entity) self._entity = entity self._input_entity = get_input_peer(entity) if entity else None if not draft or not isinstance(draft, DraftMessage): draft = DraftMessage('', None, None, None, None) self._text = markdown.unparse(draft.message, draft.entities) self._raw_text = draft.message self.date = draft.date self.link_preview = not draft.no_webpage self.reply_to_msg_id = draft.reply_to.reply_to_msg_id if isinstance(draft.reply_to, types.InputReplyToMessage) else None @property def entity(self): """ The entity that belongs to this dialog (user, chat or channel). """ return self._entity @property def input_entity(self): """ Input version of the entity. """ if not self._input_entity: try: self._input_entity = self._client._mb_entity_cache.get( get_peer_id(self._peer, add_mark=False))._as_input_peer() except AttributeError: pass return self._input_entity async def get_entity(self): """ Returns `entity` but will make an API call if necessary. """ if not self.entity and await self.get_input_entity(): try: self._entity =\ await self._client.get_entity(self._input_entity) except ValueError: pass return self._entity async def get_input_entity(self): """ Returns `input_entity` but will make an API call if necessary. """ # We don't actually have an API call we can make yet # to get more info, but keep this method for consistency. return self.input_entity @property def text(self): """ The markdown text contained in the draft. It will be empty if there is no text (and hence no draft is set). """ return self._text @property def raw_text(self): """ The raw (text without formatting) contained in the draft. It will be empty if there is no text (thus draft not set). """ return self._raw_text @property def is_empty(self): """ Convenience bool to determine if the draft is empty or not. """ return not self._text async def set_message( self, text=None, reply_to=0, parse_mode=(), link_preview=None): """ Changes the draft message on the Telegram servers. The changes are reflected in this object. :param str text: New text of the draft. Preserved if left as None. :param int reply_to: Message ID to reply to. Preserved if left as 0, erased if set to None. :param bool link_preview: Whether to attach a web page preview. Preserved if left as None. :param str parse_mode: The parse mode to be used for the text. :return bool: `True` on success. """ if text is None: text = self._text if reply_to == 0: reply_to = self.reply_to_msg_id if link_preview is None: link_preview = self.link_preview raw_text, entities =\ await self._client._parse_message_text(text, parse_mode) result = await self._client(SaveDraftRequest( peer=self._peer, message=raw_text, no_webpage=not link_preview, reply_to=None if reply_to is None else types.InputReplyToMessage(reply_to), entities=entities )) if result: self._text = text self._raw_text = raw_text self.link_preview = link_preview self.reply_to_msg_id = reply_to self.date = datetime.datetime.now(tz=datetime.timezone.utc) return result async def send(self, clear=True, parse_mode=()): """ Sends the contents of this draft to the dialog. This is just a wrapper around ``send_message(dialog.input_entity, *args, **kwargs)``. """ await self._client.send_message( self._peer, self.text, reply_to=self.reply_to_msg_id, link_preview=self.link_preview, parse_mode=parse_mode, clear_draft=clear ) async def delete(self): """ Deletes this draft, and returns `True` on success. """ return await self.set_message(text='') def to_dict(self): try: entity = self.entity except RPCError as e: entity = e return { '_': 'Draft', 'text': self.text, 'entity': entity, 'date': self.date, 'link_preview': self.link_preview, 'reply_to_msg_id': self.reply_to_msg_id } def __str__(self): return TLObject.pretty_format(self.to_dict()) def stringify(self): return TLObject.pretty_format(self.to_dict(), indent=0) Telethon-1.39.0/telethon/tl/custom/file.py000066400000000000000000000102051475566265000204500ustar00rootroot00000000000000import mimetypes import os from ... import utils from ...tl import types class File: """ Convenience class over media like photos or documents, which supports accessing the attributes in a more convenient way. If any of the attributes are not present in the current media, the properties will be `None`. The original media is available through the ``media`` attribute. """ def __init__(self, media): self.media = media @property def id(self): """ The old bot-API style ``file_id`` representing this file. .. warning:: This feature has not been maintained for a long time and may not work. It will be removed in future versions. .. note:: This file ID may not work under user accounts, but should still be usable by bot accounts. You can, however, still use it to identify a file in for example a database. """ return utils.pack_bot_file_id(self.media) @property def name(self): """ The file name of this document. """ return self._from_attr(types.DocumentAttributeFilename, 'file_name') @property def ext(self): """ The extension from the mime type of this file. If the mime type is unknown, the extension from the file name (if any) will be used. """ return ( mimetypes.guess_extension(self.mime_type) or os.path.splitext(self.name or '')[-1] or None ) @property def mime_type(self): """ The mime-type of this file. """ if isinstance(self.media, types.Photo): return 'image/jpeg' elif isinstance(self.media, types.Document): return self.media.mime_type @property def width(self): """ The width in pixels of this media if it's a photo or a video. """ if isinstance(self.media, types.Photo): return max(getattr(s, 'w', 0) for s in self.media.sizes) return self._from_attr(( types.DocumentAttributeImageSize, types.DocumentAttributeVideo), 'w') @property def height(self): """ The height in pixels of this media if it's a photo or a video. """ if isinstance(self.media, types.Photo): return max(getattr(s, 'h', 0) for s in self.media.sizes) return self._from_attr(( types.DocumentAttributeImageSize, types.DocumentAttributeVideo), 'h') @property def duration(self): """ The duration in seconds of the audio or video. """ return self._from_attr(( types.DocumentAttributeAudio, types.DocumentAttributeVideo), 'duration') @property def title(self): """ The title of the song. """ return self._from_attr(types.DocumentAttributeAudio, 'title') @property def performer(self): """ The performer of the song. """ return self._from_attr(types.DocumentAttributeAudio, 'performer') @property def emoji(self): """ A string with all emoji that represent the current sticker. """ return self._from_attr(types.DocumentAttributeSticker, 'alt') @property def sticker_set(self): """ The :tl:`InputStickerSet` to which the sticker file belongs. """ return self._from_attr(types.DocumentAttributeSticker, 'stickerset') @property def size(self): """ The size in bytes of this file. For photos, this is the heaviest thumbnail, as it often repressents the largest dimensions. """ if isinstance(self.media, types.Photo): return max(filter(None, map(utils._photo_size_byte_count, self.media.sizes)), default=None) elif isinstance(self.media, types.Document): return self.media.size def _from_attr(self, cls, field): if isinstance(self.media, types.Document): for attr in self.media.attributes: if isinstance(attr, cls): return getattr(attr, field, None) Telethon-1.39.0/telethon/tl/custom/forward.py000066400000000000000000000041211475566265000211750ustar00rootroot00000000000000from .chatgetter import ChatGetter from .sendergetter import SenderGetter from ... import utils, helpers from ...tl import types class Forward(ChatGetter, SenderGetter): """ Custom class that encapsulates a :tl:`MessageFwdHeader` providing an abstraction to easily access information like the original sender. Remember that this class implements `ChatGetter ` and `SenderGetter ` which means you have access to all their sender and chat properties and methods. Attributes: original_fwd (:tl:`MessageFwdHeader`): The original :tl:`MessageFwdHeader` instance. Any other attribute: Attributes not described here are the same as those available in the original :tl:`MessageFwdHeader`. """ def __init__(self, client, original, entities): # Copy all the fields, not reference! It would cause memory cycles: # self.original_fwd.original_fwd.original_fwd.original_fwd # ...would be valid if we referenced. self.__dict__.update(original.__dict__) self.original_fwd = original sender_id = sender = input_sender = peer = chat = input_chat = None if original.from_id: ty = helpers._entity_type(original.from_id) if ty == helpers._EntityType.USER: sender_id = utils.get_peer_id(original.from_id) sender, input_sender = utils._get_entity_pair( sender_id, entities, client._mb_entity_cache) elif ty in (helpers._EntityType.CHAT, helpers._EntityType.CHANNEL): peer = original.from_id chat, input_chat = utils._get_entity_pair( utils.get_peer_id(peer), entities, client._mb_entity_cache) # This call resets the client ChatGetter.__init__(self, peer, chat=chat, input_chat=input_chat) SenderGetter.__init__(self, sender_id, sender=sender, input_sender=input_sender) self._client = client # TODO We could reload the message Telethon-1.39.0/telethon/tl/custom/inlinebuilder.py000066400000000000000000000411631475566265000223650ustar00rootroot00000000000000import hashlib from .. import functions, types from ... import utils _TYPE_TO_MIMES = { 'gif': ['image/gif'], # 'video/mp4' too, but that's used for video 'article': ['text/html'], 'audio': ['audio/mpeg'], 'contact': [], 'file': ['application/pdf', 'application/zip'], # actually any 'geo': [], 'photo': ['image/jpeg'], 'sticker': ['image/webp', 'application/x-tgsticker'], 'venue': [], 'video': ['video/mp4'], # tdlib includes text/html for some reason 'voice': ['audio/ogg'], } class InlineBuilder: """ Helper class to allow defining `InlineQuery ` ``results``. Common arguments to all methods are explained here to avoid repetition: text (`str`, optional): If present, the user will send a text message with this text upon being clicked. link_preview (`bool`, optional): Whether to show a link preview in the sent text message or not. geo (:tl:`InputGeoPoint`, :tl:`GeoPoint`, :tl:`InputMediaVenue`, :tl:`MessageMediaVenue`, optional): If present, it may either be a geo point or a venue. period (int, optional): The period in seconds to be used for geo points. contact (:tl:`InputMediaContact`, :tl:`MessageMediaContact`, optional): If present, it must be the contact information to send. game (`bool`, optional): May be `True` to indicate that the game will be sent. buttons (`list`, `custom.Button `, :tl:`KeyboardButton`, optional): Same as ``buttons`` for `client.send_message() `. parse_mode (`str`, optional): Same as ``parse_mode`` for `client.send_message() `. id (`str`, optional): The string ID to use for this result. If not present, it will be the SHA256 hexadecimal digest of converting the created :tl:`InputBotInlineResult` with empty ID to ``bytes()``, so that the ID will be deterministic for the same input. .. note:: If two inputs are exactly the same, their IDs will be the same too. If you send two articles with the same ID, it will raise ``ResultIdDuplicateError``. Consider giving them an explicit ID if you need to send two results that are the same. """ def __init__(self, client): self._client = client # noinspection PyIncorrectDocstring async def article( self, title, description=None, *, url=None, thumb=None, content=None, id=None, text=None, parse_mode=(), link_preview=True, geo=None, period=60, contact=None, game=False, buttons=None ): """ Creates new inline result of article type. Args: title (`str`): The title to be shown for this result. description (`str`, optional): Further explanation of what this result means. url (`str`, optional): The URL to be shown for this result. thumb (:tl:`InputWebDocument`, optional): The thumbnail to be shown for this result. For now it has to be a :tl:`InputWebDocument` if present. content (:tl:`InputWebDocument`, optional): The content to be shown for this result. For now it has to be a :tl:`InputWebDocument` if present. Example: .. code-block:: python results = [ # Option with title and description sending a message. builder.article( title='First option', description='This is the first option', text='Text sent after clicking this option', ), # Option with title URL to be opened when clicked. builder.article( title='Second option', url='https://example.com', text='Text sent if the user clicks the option and not the URL', ), # Sending a message with buttons. # You can use a list or a list of lists to include more buttons. builder.article( title='Third option', text='Text sent with buttons below', buttons=Button.url('https://example.com'), ), ] """ # TODO Does 'article' work always? # article, photo, gif, mpeg4_gif, video, audio, # voice, document, location, venue, contact, game result = types.InputBotInlineResult( id=id or '', type='article', send_message=await self._message( text=text, parse_mode=parse_mode, link_preview=link_preview, geo=geo, period=period, contact=contact, game=game, buttons=buttons ), title=title, description=description, url=url, thumb=thumb, content=content ) if id is None: result.id = hashlib.sha256(bytes(result)).hexdigest() return result # noinspection PyIncorrectDocstring async def photo( self, file, *, id=None, include_media=True, text=None, parse_mode=(), link_preview=True, geo=None, period=60, contact=None, game=False, buttons=None ): """ Creates a new inline result of photo type. Args: include_media (`bool`, optional): Whether the photo file used to display the result should be included in the message itself or not. By default, the photo is included, and the text parameter alters the caption. file (`obj`, optional): Same as ``file`` for `client.send_file() `. Example: .. code-block:: python results = [ # Sending just the photo when the user selects it. builder.photo('/path/to/photo.jpg'), # Including a caption with some in-memory photo. photo_bytesio = ... builder.photo( photo_bytesio, text='This will be the caption of the sent photo', ), # Sending just the message without including the photo. builder.photo( photo, text='This will be a normal text message', include_media=False, ), ] """ try: fh = utils.get_input_photo(file) except TypeError: _, media, _ = await self._client._file_to_media( file, allow_cache=True, as_image=True ) if isinstance(media, types.InputPhoto): fh = media else: r = await self._client(functions.messages.UploadMediaRequest( types.InputPeerSelf(), media=media )) fh = utils.get_input_photo(r.photo) result = types.InputBotInlineResultPhoto( id=id or '', type='photo', photo=fh, send_message=await self._message( text=text or '', parse_mode=parse_mode, link_preview=link_preview, media=include_media, geo=geo, period=period, contact=contact, game=game, buttons=buttons ) ) if id is None: result.id = hashlib.sha256(bytes(result)).hexdigest() return result # noinspection PyIncorrectDocstring async def document( self, file, title=None, *, description=None, type=None, mime_type=None, attributes=None, force_document=False, voice_note=False, video_note=False, use_cache=True, id=None, text=None, parse_mode=(), link_preview=True, geo=None, period=60, contact=None, game=False, buttons=None, include_media=True ): """ Creates a new inline result of document type. `use_cache`, `mime_type`, `attributes`, `force_document`, `voice_note` and `video_note` are described in `client.send_file `. Args: file (`obj`): Same as ``file`` for `client.send_file() `. title (`str`, optional): The title to be shown for this result. description (`str`, optional): Further explanation of what this result means. type (`str`, optional): The type of the document. May be one of: article, audio, contact, file, geo, gif, photo, sticker, venue, video, voice. It will be automatically set if ``mime_type`` is specified, and default to ``'file'`` if no matching mime type is found. you may need to pass ``attributes`` in order to use ``type`` effectively. attributes (`list`, optional): Optional attributes that override the inferred ones, like :tl:`DocumentAttributeFilename` and so on. include_media (`bool`, optional): Whether the document file used to display the result should be included in the message itself or not. By default, the document is included, and the text parameter alters the caption. Example: .. code-block:: python results = [ # Sending just the file when the user selects it. builder.document('/path/to/file.pdf'), # Including a caption with some in-memory file. file_bytesio = ... builder.document( file_bytesio, text='This will be the caption of the sent file', ), # Sending just the message without including the file. builder.document( photo, text='This will be a normal text message', include_media=False, ), ] """ if type is None: if voice_note: type = 'voice' elif mime_type: for ty, mimes in _TYPE_TO_MIMES.items(): for mime in mimes: if mime_type == mime: type = ty break if type is None: type = 'file' try: fh = utils.get_input_document(file) except TypeError: _, media, _ = await self._client._file_to_media( file, mime_type=mime_type, attributes=attributes, force_document=force_document, voice_note=voice_note, video_note=video_note, allow_cache=use_cache ) if isinstance(media, types.InputDocument): fh = media else: r = await self._client(functions.messages.UploadMediaRequest( types.InputPeerSelf(), media=media )) fh = utils.get_input_document(r.document) result = types.InputBotInlineResultDocument( id=id or '', type=type, document=fh, send_message=await self._message( # Empty string for text if there's media but text is None. # We may want to display a document but send text; however # default to sending the media (without text, i.e. stickers). text=text or '', parse_mode=parse_mode, link_preview=link_preview, media=include_media, geo=geo, period=period, contact=contact, game=game, buttons=buttons ), title=title, description=description ) if id is None: result.id = hashlib.sha256(bytes(result)).hexdigest() return result # noinspection PyIncorrectDocstring async def game( self, short_name, *, id=None, text=None, parse_mode=(), link_preview=True, geo=None, period=60, contact=None, game=False, buttons=None ): """ Creates a new inline result of game type. Args: short_name (`str`): The short name of the game to use. """ result = types.InputBotInlineResultGame( id=id or '', short_name=short_name, send_message=await self._message( text=text, parse_mode=parse_mode, link_preview=link_preview, geo=geo, period=period, contact=contact, game=game, buttons=buttons ) ) if id is None: result.id = hashlib.sha256(bytes(result)).hexdigest() return result async def _message( self, *, text=None, parse_mode=(), link_preview=True, media=False, geo=None, period=60, contact=None, game=False, buttons=None ): # Empty strings are valid but false-y; if they're empty use dummy '\0' args = ('\0' if text == '' else text, geo, contact, game) if sum(1 for x in args if x is not None and x is not False) != 1: raise ValueError( 'Must set exactly one of text, geo, contact or game (set {})' .format(', '.join(x[0] for x in zip( 'text geo contact game'.split(), args) if x[1]) or 'none') ) markup = self._client.build_reply_markup(buttons, inline_only=True) if text is not None: text, msg_entities = await self._client._parse_message_text( text, parse_mode ) if media: # "MediaAuto" means it will use whatever media the inline # result itself has (stickers, photos, or documents), while # respecting the user's text (caption) and formatting. return types.InputBotInlineMessageMediaAuto( message=text, entities=msg_entities, reply_markup=markup ) else: return types.InputBotInlineMessageText( message=text, no_webpage=not link_preview, entities=msg_entities, reply_markup=markup ) elif isinstance(geo, (types.InputGeoPoint, types.GeoPoint)): return types.InputBotInlineMessageMediaGeo( geo_point=utils.get_input_geo(geo), period=period, reply_markup=markup ) elif isinstance(geo, (types.InputMediaVenue, types.MessageMediaVenue)): if isinstance(geo, types.InputMediaVenue): geo_point = geo.geo_point else: geo_point = geo.geo return types.InputBotInlineMessageMediaVenue( geo_point=geo_point, title=geo.title, address=geo.address, provider=geo.provider, venue_id=geo.venue_id, venue_type=geo.venue_type, reply_markup=markup ) elif isinstance(contact, ( types.InputMediaContact, types.MessageMediaContact)): return types.InputBotInlineMessageMediaContact( phone_number=contact.phone_number, first_name=contact.first_name, last_name=contact.last_name, vcard=contact.vcard, reply_markup=markup ) elif game: return types.InputBotInlineMessageGame( reply_markup=markup ) else: raise ValueError('No text, game or valid geo or contact given') Telethon-1.39.0/telethon/tl/custom/inlineresult.py000066400000000000000000000142401475566265000222510ustar00rootroot00000000000000from .. import types, functions from ... import utils class InlineResult: """ Custom class that encapsulates a bot inline result providing an abstraction to easily access some commonly needed features (such as clicking a result to select it). Attributes: result (:tl:`BotInlineResult`): The original :tl:`BotInlineResult` object. """ # tdlib types are the following (InlineQueriesManager::answer_inline_query @ 1a4a834): # gif, article, audio, contact, file, geo, photo, sticker, venue, video, voice # # However, those documented in https://core.telegram.org/bots/api#inline-mode are different. ARTICLE = 'article' PHOTO = 'photo' GIF = 'gif' VIDEO = 'video' VIDEO_GIF = 'mpeg4_gif' AUDIO = 'audio' DOCUMENT = 'document' LOCATION = 'location' VENUE = 'venue' CONTACT = 'contact' GAME = 'game' def __init__(self, client, original, query_id=None, *, entity=None): self._client = client self.result = original self._query_id = query_id self._entity = entity @property def type(self): """ The always-present type of this result. It will be one of: ``'article'``, ``'photo'``, ``'gif'``, ``'mpeg4_gif'``, ``'video'``, ``'audio'``, ``'voice'``, ``'document'``, ``'location'``, ``'venue'``, ``'contact'``, ``'game'``. You can access all of these constants through `InlineResult`, such as `InlineResult.ARTICLE`, `InlineResult.VIDEO_GIF`, etc. """ return self.result.type @property def message(self): """ The always-present :tl:`BotInlineMessage` that will be sent if `click` is called on this result. """ return self.result.send_message @property def title(self): """ The title for this inline result. It may be `None`. """ return self.result.title @property def description(self): """ The description for this inline result. It may be `None`. """ return self.result.description @property def url(self): """ The URL present in this inline results. If you want to "click" this URL to open it in your browser, you should use Python's `webbrowser.open(url)` for such task. """ if isinstance(self.result, types.BotInlineResult): return self.result.url @property def photo(self): """ Returns either the :tl:`WebDocument` thumbnail for normal results or the :tl:`Photo` for media results. """ if isinstance(self.result, types.BotInlineResult): return self.result.thumb elif isinstance(self.result, types.BotInlineMediaResult): return self.result.photo @property def document(self): """ Returns either the :tl:`WebDocument` content for normal results or the :tl:`Document` for media results. """ if isinstance(self.result, types.BotInlineResult): return self.result.content elif isinstance(self.result, types.BotInlineMediaResult): return self.result.document async def click(self, entity=None, reply_to=None, comment_to=None, silent=False, clear_draft=False, hide_via=False, background=None): """ Clicks this result and sends the associated `message`. Args: entity (`entity`): The entity to which the message of this result should be sent. reply_to (`int` | `Message `, optional): If present, the sent message will reply to this ID or message. comment_to (`int` | `Message `, optional): Similar to ``reply_to``, but replies in the linked group of a broadcast channel instead (effectively leaving a "comment to" the specified message). silent (`bool`, optional): Whether the message should notify people with sound or not. Defaults to `False` (send with a notification sound unless the person has the chat muted). Set it to `True` to alter this behaviour. clear_draft (`bool`, optional): Whether the draft should be removed after sending the message from this result or not. Defaults to `False`. hide_via (`bool`, optional): Whether the "via @bot" should be hidden or not. Only works with certain bots (like @bing or @gif). background (`bool`, optional): Whether the message should be send in background. """ if entity: entity = await self._client.get_input_entity(entity) elif self._entity: entity = self._entity else: raise ValueError('You must provide the entity where the result should be sent to') if comment_to: entity, reply_id = await self._client._get_comment_data(entity, comment_to) else: reply_id = None if reply_to is None else utils.get_message_id(reply_to) req = functions.messages.SendInlineBotResultRequest( peer=entity, query_id=self._query_id, id=self.result.id, silent=silent, background=background, clear_draft=clear_draft, hide_via=hide_via, reply_to=None if reply_id is None else types.InputReplyToMessage(reply_id) ) return self._client._get_response_message( req, await self._client(req), entity) async def download_media(self, *args, **kwargs): """ Downloads the media in this result (if there is a document, the document will be downloaded; otherwise, the photo will if present). This is a wrapper around `client.download_media `. """ if self.document or self.photo: return await self._client.download_media( self.document or self.photo, *args, **kwargs) Telethon-1.39.0/telethon/tl/custom/inlineresults.py000066400000000000000000000053021475566265000224330ustar00rootroot00000000000000import time from .inlineresult import InlineResult class InlineResults(list): """ Custom class that encapsulates :tl:`BotResults` providing an abstraction to easily access some commonly needed features (such as clicking one of the results to select it) Note that this is a list of `InlineResult ` so you can iterate over it or use indices to access its elements. In addition, it has some attributes. Attributes: result (:tl:`BotResults`): The original :tl:`BotResults` object. query_id (`int`): The random ID that identifies this query. cache_time (`int`): For how long the results should be considered valid. You can call `results_valid` at any moment to determine if the results are still valid or not. users (:tl:`User`): The users present in this inline query. gallery (`bool`): Whether these results should be presented in a grid (as a gallery of images) or not. next_offset (`str`, optional): The string to be used as an offset to get the next chunk of results, if any. switch_pm (:tl:`InlineBotSwitchPM`, optional): If presents, the results should show a button to switch to a private conversation with the bot using the text in this object. """ def __init__(self, client, original, *, entity=None): super().__init__(InlineResult(client, x, original.query_id, entity=entity) for x in original.results) self.result = original self.query_id = original.query_id self.cache_time = original.cache_time self._valid_until = time.time() + self.cache_time self.users = original.users self.gallery = bool(original.gallery) self.next_offset = original.next_offset self.switch_pm = original.switch_pm def results_valid(self): """ Returns `True` if the cache time has not expired yet and the results can still be considered valid. """ return time.time() < self._valid_until def _to_str(self, item_function): return ('[{}, query_id={}, cache_time={}, users={}, gallery={}, ' 'next_offset={}, switch_pm={}]'.format( ', '.join(item_function(x) for x in self), self.query_id, self.cache_time, self.users, self.gallery, self.next_offset, self.switch_pm )) def __str__(self): return self._to_str(str) def __repr__(self): return self._to_str(repr) Telethon-1.39.0/telethon/tl/custom/inputsizedfile.py000066400000000000000000000004661475566265000225770ustar00rootroot00000000000000from ..types import InputFile class InputSizedFile(InputFile): """InputFile class with two extra parameters: md5 (digest) and size""" def __init__(self, id_, parts, name, md5, size): super().__init__(id_, parts, name, md5.hexdigest()) self.md5 = md5.digest() self.size = size Telethon-1.39.0/telethon/tl/custom/message.py000066400000000000000000001305511475566265000211640ustar00rootroot00000000000000from typing import Optional, List, TYPE_CHECKING from datetime import datetime from .chatgetter import ChatGetter from .sendergetter import SenderGetter from .messagebutton import MessageButton from .forward import Forward from .file import File from .. import TLObject, types, functions, alltlobjects from ... import utils, errors # TODO Figure out a way to have the code generator error on missing fields # Maybe parsing the init function alone if that's possible. class Message(ChatGetter, SenderGetter, TLObject): """ This custom class aggregates both :tl:`Message` and :tl:`MessageService` to ease accessing their members. Remember that this class implements `ChatGetter ` and `SenderGetter ` which means you have access to all their sender and chat properties and methods. Members: out (`bool`): Whether the message is outgoing (i.e. you sent it from another session) or incoming (i.e. someone else sent it). Note that messages in your own chat are always incoming, but this member will be `True` if you send a message to your own chat. Messages you forward to your chat are *not* considered outgoing, just like official clients display them. mentioned (`bool`): Whether you were mentioned in this message or not. Note that replies to your own messages also count as mentions. media_unread (`bool`): Whether you have read the media in this message or not, e.g. listened to the voice note media. silent (`bool`): Whether the message should notify people with sound or not. Previously used in channels, but since 9 August 2019, it can also be `used in private chats `_. post (`bool`): Whether this message is a post in a broadcast channel or not. from_scheduled (`bool`): Whether this message was originated from a previously-scheduled message or not. legacy (`bool`): Whether this is a legacy message or not. edit_hide (`bool`): Whether the edited mark of this message is edited should be hidden (e.g. in GUI clients) or shown. pinned (`bool`): Whether this message is currently pinned or not. noforwards (`bool`): Whether this message can be forwarded or not. invert_media (`bool`): Whether the media in this message should be inverted. offline (`bool`): Whether the message was sent by an implicit action, for example, as an away or a greeting business message, or as a scheduled message. id (`int`): The ID of this message. This field is *always* present. Any other member is optional and may be `None`. from_id (:tl:`Peer`): The peer who sent this message, which is either :tl:`PeerUser`, :tl:`PeerChat` or :tl:`PeerChannel`. This value will be `None` for anonymous messages. peer_id (:tl:`Peer`): The peer to which this message was sent, which is either :tl:`PeerUser`, :tl:`PeerChat` or :tl:`PeerChannel`. This will always be present except for empty messages. fwd_from (:tl:`MessageFwdHeader`): The original forward header if this message is a forward. You should probably use the `forward` property instead. via_bot_id (`int`): The ID of the bot used to send this message through its inline mode (e.g. "via @like"). reply_to (:tl:`MessageReplyHeader`): The original reply header if this message is replying to another. date (`datetime`): The UTC+0 `datetime` object indicating when this message was sent. This will always be present except for empty messages. message (`str`): The string text of the message for `Message ` instances, which will be `None` for other types of messages. media (:tl:`MessageMedia`): The media sent with this message if any (such as photos, videos, documents, gifs, stickers, etc.). You may want to access the `photo`, `document` etc. properties instead. If the media was not present or it was :tl:`MessageMediaEmpty`, this member will instead be `None` for convenience. reply_markup (:tl:`ReplyMarkup`): The reply markup for this message (which was sent either via a bot or by a bot). You probably want to access `buttons` instead. entities (List[:tl:`MessageEntity`]): The list of markup entities in this message, such as bold, italics, code, hyperlinks, etc. views (`int`): The number of views this message from a broadcast channel has. This is also present in forwards. forwards (`int`): The number of times this message has been forwarded. replies (`int`): The number of times another message has replied to this message. edit_date (`datetime`): The date when this message was last edited. post_author (`str`): The display name of the message sender to show in messages sent to broadcast channels. grouped_id (`int`): If this message belongs to a group of messages (photo albums or video albums), all of them will have the same value here. reactions (:tl:`MessageReactions`) Reactions to this message. restriction_reason (List[:tl:`RestrictionReason`]) An optional list of reasons why this message was restricted. If the list is `None`, this message has not been restricted. ttl_period (`int`): The Time To Live period configured for this message. The message should be erased from wherever it's stored (memory, a local database, etc.) when ``datetime.now() > message.date + timedelta(seconds=message.ttl_period)``. action (:tl:`MessageAction`): The message action object of the message for :tl:`MessageService` instances, which will be `None` for other types of messages. saved_peer_id (:tl:`Peer`) """ # region Initialization def __init__( self, id: int, peer_id: types.TypePeer, date: Optional[datetime]=None, message: Optional[str]=None, # Copied from Message.__init__ signature out: Optional[bool]=None, mentioned: Optional[bool]=None, media_unread: Optional[bool]=None, silent: Optional[bool]=None, post: Optional[bool]=None, from_scheduled: Optional[bool]=None, legacy: Optional[bool]=None, edit_hide: Optional[bool]=None, pinned: Optional[bool]=None, noforwards: Optional[bool]=None, invert_media: Optional[bool]=None, offline: Optional[bool]=None, video_processing_pending: Optional[bool]=None, from_id: Optional[types.TypePeer]=None, from_boosts_applied: Optional[int]=None, saved_peer_id: Optional[types.TypePeer]=None, fwd_from: Optional[types.TypeMessageFwdHeader]=None, via_bot_id: Optional[int]=None, via_business_bot_id: Optional[int]=None, reply_to: Optional[types.TypeMessageReplyHeader]=None, media: Optional[types.TypeMessageMedia]=None, reply_markup: Optional[types.TypeReplyMarkup]=None, entities: Optional[List[types.TypeMessageEntity]]=None, views: Optional[int]=None, forwards: Optional[int]=None, replies: Optional[types.TypeMessageReplies]=None, edit_date: Optional[datetime]=None, post_author: Optional[str]=None, grouped_id: Optional[int]=None, reactions: Optional[types.TypeMessageReactions]=None, restriction_reason: Optional[List[types.TypeRestrictionReason]]=None, ttl_period: Optional[int]=None, quick_reply_shortcut_id: Optional[int]=None, effect: Optional[int]=None, factcheck: Optional[types.TypeFactCheck]=None, report_delivery_until_date: Optional[datetime]=None, # Copied from MessageService.__init__ signature action: Optional[types.TypeMessageAction]=None, reactions_are_possible: Optional[bool]=None ): # Copied from Message.__init__ body self.id = id self.peer_id = peer_id self.date = date self.message = message self.out = bool(out) self.mentioned = mentioned self.media_unread = media_unread self.silent = silent self.post = post self.from_scheduled = from_scheduled self.legacy = legacy self.edit_hide = edit_hide self.pinned = pinned self.noforwards = noforwards self.invert_media = invert_media self.offline = offline self.video_processing_pending = video_processing_pending self.from_id = from_id self.from_boosts_applied = from_boosts_applied self.saved_peer_id = saved_peer_id self.fwd_from = fwd_from self.via_bot_id = via_bot_id self.via_business_bot_id = via_business_bot_id self.reply_to = reply_to self.media = None if isinstance(media, types.MessageMediaEmpty) else media self.reply_markup = reply_markup self.entities = entities self.views = views self.forwards = forwards self.replies = replies self.edit_date = edit_date self.post_author = post_author self.grouped_id = grouped_id self.reactions = reactions self.restriction_reason = restriction_reason self.ttl_period = ttl_period self.quick_reply_shortcut_id = quick_reply_shortcut_id self.effect = effect self.factcheck = factcheck self.report_delivery_until_date = report_delivery_until_date # Copied from MessageService.__init__ body self.action = action self.reactions_are_possible = reactions_are_possible # Convenient storage for custom functions # TODO This is becoming a bit of bloat self._client = None self._text = None self._file = None self._reply_message = None self._buttons = None self._buttons_flat = None self._buttons_count = None self._via_bot = None self._via_input_bot = None self._action_entities = None self._linked_chat = None sender_id = None if from_id is not None: sender_id = utils.get_peer_id(from_id) elif peer_id: # If the message comes from a Channel, let the sender be it # ...or... # incoming messages in private conversations no longer have from_id # (layer 119+), but the sender can only be the chat we're in. if post or (not out and isinstance(peer_id, types.PeerUser)): sender_id = utils.get_peer_id(peer_id) # Note that these calls would reset the client ChatGetter.__init__(self, peer_id, broadcast=post) SenderGetter.__init__(self, sender_id) self._forward = None self._reply_to_chat = None self._reply_to_sender = None def _finish_init(self, client, entities, input_chat): """ Finishes the initialization of this message by setting the client that sent the message and making use of the known entities. """ self._client = client # Make messages sent to ourselves outgoing unless they're forwarded. # This makes it consistent with official client's appearance. if self.peer_id == types.PeerUser(client._self_id) and not self.fwd_from: self.out = True cache = client._mb_entity_cache self._sender, self._input_sender = utils._get_entity_pair( self.sender_id, entities, cache) self._chat, self._input_chat = utils._get_entity_pair( self.chat_id, entities, cache) if input_chat: # This has priority self._input_chat = input_chat if self.via_bot_id: self._via_bot, self._via_input_bot = utils._get_entity_pair( self.via_bot_id, entities, cache) if self.fwd_from: self._forward = Forward(self._client, self.fwd_from, entities) if self.action: if isinstance(self.action, (types.MessageActionChatAddUser, types.MessageActionChatCreate)): self._action_entities = [entities.get(i) for i in self.action.users] elif isinstance(self.action, types.MessageActionChatDeleteUser): self._action_entities = [entities.get(self.action.user_id)] elif isinstance(self.action, types.MessageActionChatJoinedByLink): self._action_entities = [entities.get(self.action.inviter_id)] elif isinstance(self.action, types.MessageActionChatMigrateTo): self._action_entities = [entities.get(utils.get_peer_id( types.PeerChannel(self.action.channel_id)))] elif isinstance( self.action, types.MessageActionChannelMigrateFrom): self._action_entities = [entities.get(utils.get_peer_id( types.PeerChat(self.action.chat_id)))] if self.replies and self.replies.channel_id: self._linked_chat = entities.get(utils.get_peer_id( types.PeerChannel(self.replies.channel_id))) if isinstance(self.reply_to, types.MessageReplyHeader): if self.reply_to.reply_to_peer_id: self._reply_to_chat = entities.get(utils.get_peer_id(self.reply_to.reply_to_peer_id)) if self.reply_to.reply_from: if self.reply_to.reply_from.from_id: self._reply_to_sender = entities.get(utils.get_peer_id(self.reply_to.reply_from.from_id)) # endregion Initialization # region Public Properties @property def client(self): """ Returns the `TelegramClient ` that *patched* this message. This will only be present if you **use the friendly methods**, it won't be there if you invoke raw API methods manually, in which case you should only access members, not properties. """ return self._client @property def text(self): """ The message text, formatted using the client's default parse mode. Will be `None` for :tl:`MessageService`. """ if self._text is None and self._client: if not self._client.parse_mode: self._text = self.message else: self._text = self._client.parse_mode.unparse( self.message, self.entities) return self._text @text.setter def text(self, value): self._text = value if self._client and self._client.parse_mode: self.message, self.entities = self._client.parse_mode.parse(value) else: self.message, self.entities = value, [] @property def raw_text(self): """ The raw message text, ignoring any formatting. Will be `None` for :tl:`MessageService`. Setting a value to this field will erase the `entities`, unlike changing the `message` member. """ return self.message @raw_text.setter def raw_text(self, value): self.message = value self.entities = [] self._text = None @property def is_reply(self): """ `True` if the message is a reply to some other message. Remember that you can access the ID of the message this one is replying to through `reply_to.reply_to_msg_id`, and the `Message` object with `get_reply_message()`. """ return self.reply_to is not None @property def forward(self): """ The `Forward ` information if this message is a forwarded message. """ return self._forward @property def reply_to_chat(self): """ The :tl:`Channel` in which the replied-to message was sent, if this message is a reply in another chat """ return self._reply_to_chat @property def reply_to_sender(self): """ The :tl:`User`, :tl:`Channel`, or whatever other entity that sent the replied-to message, if this message is a reply in another chat. """ return self._reply_to_sender @property def buttons(self): """ Returns a list of lists of `MessageButton `, if any. Otherwise, it returns `None`. """ if self._buttons is None and self.reply_markup: if not self.input_chat: return try: bot = self._needed_markup_bot() except ValueError: return else: self._set_buttons(self._input_chat, bot) return self._buttons async def get_buttons(self): """ Returns `buttons` when that property fails (this is rarely needed). """ if not self.buttons and self.reply_markup: chat = await self.get_input_chat() if not chat: return try: bot = self._needed_markup_bot() except ValueError: await self._reload_message() bot = self._needed_markup_bot() # TODO use via_input_bot self._set_buttons(chat, bot) return self._buttons @property def button_count(self): """ Returns the total button count (sum of all `buttons` rows). """ if self._buttons_count is None: if isinstance(self.reply_markup, ( types.ReplyInlineMarkup, types.ReplyKeyboardMarkup)): self._buttons_count = sum( len(row.buttons) for row in self.reply_markup.rows) else: self._buttons_count = 0 return self._buttons_count @property def file(self): """ Returns a `File ` wrapping the `photo` or `document` in this message. If the media type is different (polls, games, none, etc.), this property will be `None`. This instance lets you easily access other properties, such as `file.id `, `file.name `, etc., without having to manually inspect the ``document.attributes``. """ if not self._file: media = self.photo or self.document if media: self._file = File(media) return self._file @property def photo(self): """ The :tl:`Photo` media in this message, if any. This will also return the photo for :tl:`MessageService` if its action is :tl:`MessageActionChatEditPhoto`, or if the message has a web preview with a photo. """ if isinstance(self.media, types.MessageMediaPhoto): if isinstance(self.media.photo, types.Photo): return self.media.photo elif isinstance(self.action, types.MessageActionChatEditPhoto): return self.action.photo else: web = self.web_preview if web and isinstance(web.photo, types.Photo): return web.photo @property def document(self): """ The :tl:`Document` media in this message, if any. """ if isinstance(self.media, types.MessageMediaDocument): if isinstance(self.media.document, types.Document): return self.media.document else: web = self.web_preview if web and isinstance(web.document, types.Document): return web.document @property def web_preview(self): """ The :tl:`WebPage` media in this message, if any. """ if isinstance(self.media, types.MessageMediaWebPage): if isinstance(self.media.webpage, types.WebPage): return self.media.webpage @property def audio(self): """ The :tl:`Document` media in this message, if it's an audio file. """ return self._document_by_attribute(types.DocumentAttributeAudio, lambda attr: not attr.voice) @property def voice(self): """ The :tl:`Document` media in this message, if it's a voice note. """ return self._document_by_attribute(types.DocumentAttributeAudio, lambda attr: attr.voice) @property def video(self): """ The :tl:`Document` media in this message, if it's a video. """ return self._document_by_attribute(types.DocumentAttributeVideo) @property def video_note(self): """ The :tl:`Document` media in this message, if it's a video note. """ return self._document_by_attribute(types.DocumentAttributeVideo, lambda attr: attr.round_message) @property def gif(self): """ The :tl:`Document` media in this message, if it's a "gif". "Gif" files by Telegram are normally ``.mp4`` video files without sound, the so called "animated" media. However, it may be the actual gif format if the file is too large. """ return self._document_by_attribute(types.DocumentAttributeAnimated) @property def sticker(self): """ The :tl:`Document` media in this message, if it's a sticker. """ return self._document_by_attribute(types.DocumentAttributeSticker) @property def contact(self): """ The :tl:`MessageMediaContact` in this message, if it's a contact. """ if isinstance(self.media, types.MessageMediaContact): return self.media @property def game(self): """ The :tl:`Game` media in this message, if it's a game. """ if isinstance(self.media, types.MessageMediaGame): return self.media.game @property def geo(self): """ The :tl:`GeoPoint` media in this message, if it has a location. """ if isinstance(self.media, (types.MessageMediaGeo, types.MessageMediaGeoLive, types.MessageMediaVenue)): return self.media.geo @property def invoice(self): """ The :tl:`MessageMediaInvoice` in this message, if it's an invoice. """ if isinstance(self.media, types.MessageMediaInvoice): return self.media @property def poll(self): """ The :tl:`MessageMediaPoll` in this message, if it's a poll. """ if isinstance(self.media, types.MessageMediaPoll): return self.media @property def venue(self): """ The :tl:`MessageMediaVenue` in this message, if it's a venue. """ if isinstance(self.media, types.MessageMediaVenue): return self.media @property def dice(self): """ The :tl:`MessageMediaDice` in this message, if it's a dice roll. """ if isinstance(self.media, types.MessageMediaDice): return self.media @property def action_entities(self): """ Returns a list of entities that took part in this action. Possible cases for this are :tl:`MessageActionChatAddUser`, :tl:`types.MessageActionChatCreate`, :tl:`MessageActionChatDeleteUser`, :tl:`MessageActionChatJoinedByLink` :tl:`MessageActionChatMigrateTo` and :tl:`MessageActionChannelMigrateFrom`. If the action is neither of those, the result will be `None`. If some entities could not be retrieved, the list may contain some `None` items in it. """ return self._action_entities @property def via_bot(self): """ The bot :tl:`User` if the message was sent via said bot. This will only be present if `via_bot_id` is not `None` and the entity is known. """ return self._via_bot @property def via_input_bot(self): """ Returns the input variant of `via_bot`. """ return self._via_input_bot @property def reply_to_msg_id(self): """ Returns the message ID this message is replying to, if any. This is equivalent to accessing ``.reply_to.reply_to_msg_id``. """ return self.reply_to.reply_to_msg_id if self.reply_to else None @property def to_id(self): """ Returns the peer to which this message was sent to. This used to exist to infer the ``.peer_id``. """ # If the client wasn't set we can't emulate the behaviour correctly, # so as a best-effort simply return the chat peer. if self._client and not self.out and self.is_private: return types.PeerUser(self._client._self_id) return self.peer_id # endregion Public Properties # region Public Methods def get_entities_text(self, cls=None): """ Returns a list of ``(markup entity, inner text)`` (like bold or italics). The markup entity is a :tl:`MessageEntity` that represents bold, italics, etc., and the inner text is the `str` inside that markup entity. For example: .. code-block:: python print(repr(message.text)) # shows: 'Hello **world**!' for ent, txt in message.get_entities_text(): print(ent) # shows: MessageEntityBold(offset=6, length=5) print(txt) # shows: world Args: cls (`type`): Returns entities matching this type only. For example, the following will print the text for all ``code`` entities: >>> from telethon.tl.types import MessageEntityCode >>> >>> m = ... # get the message >>> for _, inner_text in m.get_entities_text(MessageEntityCode): >>> print(inner_text) """ ent = self.entities if not ent: return [] if cls: ent = [c for c in ent if isinstance(c, cls)] texts = utils.get_inner_text(self.message, ent) return list(zip(ent, texts)) async def get_reply_message(self): """ The `Message` that this message is replying to, or `None`. The result will be cached after its first use. """ if self._reply_message is None and self._client: if not self.reply_to: return None # Bots cannot access other bots' messages by their ID. # However they can access them through replies... self._reply_message = await self._client.get_messages( await self.get_input_chat() if self.is_channel else None, ids=types.InputMessageReplyTo(self.id) ) if not self._reply_message: # ...unless the current message got deleted. # # If that's the case, give it a second chance accessing # directly by its ID. self._reply_message = await self._client.get_messages( self._input_chat if self.is_channel else None, ids=self.reply_to.reply_to_msg_id ) return self._reply_message async def respond(self, *args, **kwargs): """ Responds to the message (not as a reply). Shorthand for `telethon.client.messages.MessageMethods.send_message` with ``entity`` already set. """ if self._client: return await self._client.send_message( await self.get_input_chat(), *args, **kwargs) async def reply(self, *args, **kwargs): """ Replies to the message (as a reply). Shorthand for `telethon.client.messages.MessageMethods.send_message` with both ``entity`` and ``reply_to`` already set. """ if self._client: kwargs['reply_to'] = self.id return await self._client.send_message( await self.get_input_chat(), *args, **kwargs) async def forward_to(self, *args, **kwargs): """ Forwards the message. Shorthand for `telethon.client.messages.MessageMethods.forward_messages` with both ``messages`` and ``from_peer`` already set. If you need to forward more than one message at once, don't use this `forward_to` method. Use a `telethon.client.telegramclient.TelegramClient` instance directly. """ if self._client: kwargs['messages'] = self.id kwargs['from_peer'] = await self.get_input_chat() return await self._client.forward_messages(*args, **kwargs) async def edit(self, *args, **kwargs): """ Edits the message if it's outgoing. Shorthand for `telethon.client.messages.MessageMethods.edit_message` with both ``entity`` and ``message`` already set. Returns The edited `Message `, unless `entity` was a :tl:`InputBotInlineMessageID` or :tl:`InputBotInlineMessageID64` in which case this method returns a boolean. Raises ``MessageAuthorRequiredError`` if you're not the author of the message but tried editing it anyway. ``MessageNotModifiedError`` if the contents of the message were not modified at all. ``MessageIdInvalidError`` if the ID of the message is invalid (the ID itself may be correct, but the message with that ID cannot be edited). For example, when trying to edit messages with a reply markup (or clear markup) this error will be raised. .. note:: This is different from `client.edit_message ` and **will respect** the previous state of the message. For example, if the message didn't have a link preview, the edit won't add one by default, and you should force it by setting it to `True` if you want it. This is generally the most desired and convenient behaviour, and will work for link previews and message buttons. """ if 'link_preview' not in kwargs: kwargs['link_preview'] = bool(self.web_preview) if 'buttons' not in kwargs: kwargs['buttons'] = self.reply_markup return await self._client.edit_message( await self.get_input_chat(), self.id, *args, **kwargs ) async def delete(self, *args, **kwargs): """ Deletes the message. You're responsible for checking whether you have the permission to do so, or to except the error otherwise. Shorthand for `telethon.client.messages.MessageMethods.delete_messages` with ``entity`` and ``message_ids`` already set. If you need to delete more than one message at once, don't use this `delete` method. Use a `telethon.client.telegramclient.TelegramClient` instance directly. """ if self._client: return await self._client.delete_messages( await self.get_input_chat(), [self.id], *args, **kwargs ) async def download_media(self, *args, **kwargs): """ Downloads the media contained in the message, if any. Shorthand for `telethon.client.downloads.DownloadMethods.download_media` with the ``message`` already set. """ if self._client: # Passing the entire message is important, in case it has to be # refetched for a fresh file reference. return await self._client.download_media(self, *args, **kwargs) async def click(self, i=None, j=None, *, text=None, filter=None, data=None, share_phone=None, share_geo=None, password=None): """ Calls :tl:`SendVote` with the specified poll option or `button.click ` on the specified button. Does nothing if the message is not a poll or has no buttons. Args: i (`int` | `list`): Clicks the i'th button or poll option (starting from the index 0). For multiple-choice polls, a list with the indices should be used. Will ``raise IndexError`` if out of bounds. Example: >>> message = ... # get the message somehow >>> # Clicking the 3rd button >>> # [button1] [button2] >>> # [ button3 ] >>> # [button4] [button5] >>> await message.click(2) # index j (`int`): Clicks the button at position (i, j), these being the indices for the (row, column) respectively. Example: >>> # Clicking the 2nd button on the 1st row. >>> # [button1] [button2] >>> # [ button3 ] >>> # [button4] [button5] >>> await message.click(0, 1) # (row, column) This is equivalent to ``message.buttons[0][1].click()``. text (`str` | `callable`): Clicks the first button or poll option with the text "text". This may also be a callable, like a ``re.compile(...).match``, and the text will be passed to it. If you need to select multiple options in a poll, pass a list of indices to the ``i`` parameter. filter (`callable`): Clicks the first button or poll option for which the callable returns `True`. The callable should accept a single `MessageButton ` or `PollAnswer ` argument. If you need to select multiple options in a poll, pass a list of indices to the ``i`` parameter. data (`bytes`): This argument overrides the rest and will not search any buttons. Instead, it will directly send the request to behave as if it clicked a button with said data. Note that if the message does not have this data, it will ``raise DataInvalidError``. share_phone (`bool` | `str` | tl:`InputMediaContact`): When clicking on a keyboard button requesting a phone number (:tl:`KeyboardButtonRequestPhone`), this argument must be explicitly set to avoid accidentally sharing the number. It can be `True` to automatically share the current user's phone, a string to share a specific phone number, or a contact media to specify all details. If the button is pressed without this, `ValueError` is raised. share_geo (`tuple` | `list` | tl:`InputMediaGeoPoint`): When clicking on a keyboard button requesting a geo location (:tl:`KeyboardButtonRequestGeoLocation`), this argument must be explicitly set to avoid accidentally sharing the location. It must be a `tuple` of `float` as ``(longitude, latitude)``, or a :tl:`InputGeoPoint` instance to avoid accidentally using the wrong roder. If the button is pressed without this, `ValueError` is raised. password (`str`): When clicking certain buttons (such as BotFather's confirmation button to transfer ownership), if your account has 2FA enabled, you need to provide your account's password. Otherwise, `teltehon.errors.PasswordHashInvalidError` is raised. Example: .. code-block:: python # Click the first button await message.click(0) # Click some row/column await message.click(row, column) # Click by text await message.click(text='👍') # Click by data await message.click(data=b'payload') # Click on a button requesting a phone await message.click(0, share_phone=True) """ if not self._client: return if data: chat = await self.get_input_chat() if not chat: return None but = types.KeyboardButtonCallback('', data) return await MessageButton(self._client, but, chat, None, self.id).click( share_phone=share_phone, share_geo=share_geo, password=password) if sum(int(x is not None) for x in (i, text, filter)) >= 2: raise ValueError('You can only set either of i, text or filter') # Finding the desired poll options and sending them if self.poll is not None: def find_options(): answers = self.poll.poll.answers if i is not None: if utils.is_list_like(i): return [answers[idx].option for idx in i] return [answers[i].option] if text is not None: if callable(text): for answer in answers: if text(answer.text): return [answer.option] else: for answer in answers: if answer.text == text: return [answer.option] return if filter is not None: for answer in answers: if filter(answer): return [answer.option] return options = find_options() if options is None: options = [] return await self._client( functions.messages.SendVoteRequest( peer=self._input_chat, msg_id=self.id, options=options ) ) if not await self.get_buttons(): return # Accessing the property sets self._buttons[_flat] def find_button(): nonlocal i if text is not None: if callable(text): for button in self._buttons_flat: if text(button.text): return button else: for button in self._buttons_flat: if button.text == text: return button return if filter is not None: for button in self._buttons_flat: if filter(button): return button return if i is None: i = 0 if j is None: return self._buttons_flat[i] else: return self._buttons[i][j] button = find_button() if button: return await button.click( share_phone=share_phone, share_geo=share_geo, password=password) async def mark_read(self): """ Marks the message as read. Shorthand for `client.send_read_acknowledge() ` with both ``entity`` and ``message`` already set. """ if self._client: await self._client.send_read_acknowledge( await self.get_input_chat(), max_id=self.id) async def pin(self, *, notify=False, pm_oneside=False): """ Pins the message. Shorthand for `telethon.client.messages.MessageMethods.pin_message` with both ``entity`` and ``message`` already set. """ # TODO Constantly checking if client is a bit annoying, # maybe just make it illegal to call messages from raw API? # That or figure out a way to always set it directly. if self._client: return await self._client.pin_message( await self.get_input_chat(), self.id, notify=notify, pm_oneside=pm_oneside) async def unpin(self): """ Unpins the message. Shorthand for `telethon.client.messages.MessageMethods.unpin_message` with both ``entity`` and ``message`` already set. """ if self._client: return await self._client.unpin_message( await self.get_input_chat(), self.id) # endregion Public Methods # region Private Methods async def _reload_message(self): """ Re-fetches this message to reload the sender and chat entities, along with their input versions. """ if not self._client: return try: chat = await self.get_input_chat() if self.is_channel else None msg = await self._client.get_messages(chat, ids=self.id) except ValueError: return # We may not have the input chat/get message failed if not msg: return # The message may be deleted and it will be None self._sender = msg._sender self._input_sender = msg._input_sender self._chat = msg._chat self._input_chat = msg._input_chat self._via_bot = msg._via_bot self._via_input_bot = msg._via_input_bot self._forward = msg._forward self._action_entities = msg._action_entities async def _refetch_sender(self): await self._reload_message() def _set_buttons(self, chat, bot): """ Helper methods to set the buttons given the input sender and chat. """ if self._client and isinstance(self.reply_markup, ( types.ReplyInlineMarkup, types.ReplyKeyboardMarkup)): self._buttons = [[ MessageButton(self._client, button, chat, bot, self.id) for button in row.buttons ] for row in self.reply_markup.rows] self._buttons_flat = [x for row in self._buttons for x in row] def _needed_markup_bot(self): """ Returns the input peer of the bot that's needed for the reply markup. This is necessary for :tl:`KeyboardButtonSwitchInline` since we need to know what bot we want to start. Raises ``ValueError`` if the bot cannot be found but is needed. Returns `None` if it's not needed. """ if self._client and not isinstance(self.reply_markup, ( types.ReplyInlineMarkup, types.ReplyKeyboardMarkup)): return None for row in self.reply_markup.rows: for button in row.buttons: if isinstance(button, types.KeyboardButtonSwitchInline): # no via_bot_id means the bot sent the message itself (#1619) if button.same_peer or not self.via_bot_id: bot = self.input_sender if not bot: raise ValueError('No input sender') return bot else: try: return self._client._mb_entity_cache.get( utils.resolve_id(self.via_bot_id)[0])._as_input_peer() except AttributeError: raise ValueError('No input sender') from None def _document_by_attribute(self, kind, condition=None): """ Helper method to return the document only if it has an attribute that's an instance of the given kind, and passes the condition. """ doc = self.document if doc: for attr in doc.attributes: if isinstance(attr, kind): if not condition or condition(attr): return doc return None # endregion Private Methods Telethon-1.39.0/telethon/tl/custom/messagebutton.py000066400000000000000000000137361475566265000224250ustar00rootroot00000000000000from .. import types, functions from ... import password as pwd_mod from ...errors import BotResponseTimeoutError try: import webbrowser except ModuleNotFoundError: pass import sys import os class MessageButton: """ .. note:: `Message.buttons ` are instances of this type. If you want to **define** a reply markup for e.g. sending messages, refer to `Button ` instead. Custom class that encapsulates a message button providing an abstraction to easily access some commonly needed features (such as clicking the button itself). Attributes: button (:tl:`KeyboardButton`): The original :tl:`KeyboardButton` object. """ def __init__(self, client, original, chat, bot, msg_id): self.button = original self._bot = bot self._chat = chat self._msg_id = msg_id self._client = client @property def client(self): """ Returns the `telethon.client.telegramclient.TelegramClient` instance that created this instance. """ return self._client @property def text(self): """The text string of the button.""" return self.button.text @property def data(self): """The `bytes` data for :tl:`KeyboardButtonCallback` objects.""" if isinstance(self.button, types.KeyboardButtonCallback): return self.button.data @property def inline_query(self): """The query `str` for :tl:`KeyboardButtonSwitchInline` objects.""" if isinstance(self.button, types.KeyboardButtonSwitchInline): return self.button.query @property def url(self): """The url `str` for :tl:`KeyboardButtonUrl` objects.""" if isinstance(self.button, types.KeyboardButtonUrl): return self.button.url async def click(self, share_phone=None, share_geo=None, *, password=None): """ Emulates the behaviour of clicking this button. If it's a normal :tl:`KeyboardButton` with text, a message will be sent, and the sent `Message ` returned. If it's an inline :tl:`KeyboardButtonCallback` with text and data, it will be "clicked" and the :tl:`BotCallbackAnswer` returned. If it's an inline :tl:`KeyboardButtonSwitchInline` button, the :tl:`StartBotRequest` will be invoked and the resulting updates returned. If it's a :tl:`KeyboardButtonUrl`, the URL of the button will be passed to ``webbrowser.open`` and return `True` on success. If it's a :tl:`KeyboardButtonRequestPhone`, you must indicate that you want to ``share_phone=True`` in order to share it. Sharing it is not a default because it is a privacy concern and could happen accidentally. You may also use ``share_phone=phone`` to share a specific number, in which case either `str` or :tl:`InputMediaContact` should be used. If it's a :tl:`KeyboardButtonRequestGeoLocation`, you must pass a tuple in ``share_geo=(longitude, latitude)``. Note that Telegram seems to have some heuristics to determine impossible locations, so changing this value a lot quickly may not work as expected. You may also pass a :tl:`InputGeoPoint` if you find the order confusing. """ if isinstance(self.button, types.KeyboardButton): return await self._client.send_message( self._chat, self.button.text, parse_mode=None) elif isinstance(self.button, types.KeyboardButtonCallback): if password is not None: pwd = await self._client(functions.account.GetPasswordRequest()) password = pwd_mod.compute_check(pwd, password) req = functions.messages.GetBotCallbackAnswerRequest( peer=self._chat, msg_id=self._msg_id, data=self.button.data, password=password ) try: return await self._client(req) except BotResponseTimeoutError: return None elif isinstance(self.button, types.KeyboardButtonSwitchInline): return await self._client(functions.messages.StartBotRequest( bot=self._bot, peer=self._chat, start_param=self.button.query )) elif isinstance(self.button, types.KeyboardButtonUrl): if "webbrowser" in sys.modules: return webbrowser.open(self.button.url) elif isinstance(self.button, types.KeyboardButtonGame): req = functions.messages.GetBotCallbackAnswerRequest( peer=self._chat, msg_id=self._msg_id, game=True ) try: return await self._client(req) except BotResponseTimeoutError: return None elif isinstance(self.button, types.KeyboardButtonRequestPhone): if not share_phone: raise ValueError('cannot click on phone buttons unless share_phone=True') if share_phone == True or isinstance(share_phone, str): me = await self._client.get_me() share_phone = types.InputMediaContact( phone_number=me.phone if share_phone == True else share_phone, first_name=me.first_name or '', last_name=me.last_name or '', vcard='' ) return await self._client.send_file(self._chat, share_phone) elif isinstance(self.button, types.KeyboardButtonRequestGeoLocation): if not share_geo: raise ValueError('cannot click on geo buttons unless share_geo=(longitude, latitude)') if isinstance(share_geo, (tuple, list)): long, lat = share_geo share_geo = types.InputMediaGeoPoint(types.InputGeoPoint(lat=lat, long=long)) return await self._client.send_file(self._chat, share_geo) Telethon-1.39.0/telethon/tl/custom/participantpermissions.py000066400000000000000000000100431475566265000243430ustar00rootroot00000000000000from .. import types def _admin_prop(field_name, doc): """ Helper method to build properties that return `True` if the user is an administrator of a normal chat, or otherwise return `True` if the user has a specific permission being an admin of a channel. """ def fget(self): if not self.is_admin: return False if self.is_chat: return True return getattr(self.participant.admin_rights, field_name) return {'fget': fget, 'doc': doc} class ParticipantPermissions: """ Participant permissions information. The properties in this objects are boolean values indicating whether the user has the permission or not. Example .. code-block:: python permissions = ... if permissions.is_banned: "this user is banned" elif permissions.is_admin: "this user is an administrator" """ def __init__(self, participant, chat: bool): self.participant = participant self.is_chat = chat @property def is_admin(self): """ Whether the user is an administrator of the chat or not. The creator also counts as begin an administrator, since they have all permissions. """ return self.is_creator or isinstance(self.participant, ( types.ChannelParticipantAdmin, types.ChatParticipantAdmin )) @property def is_creator(self): """ Whether the user is the creator of the chat or not. """ return isinstance(self.participant, ( types.ChannelParticipantCreator, types.ChatParticipantCreator )) @property def has_default_permissions(self): """ Whether the user is a normal user of the chat (not administrator, but not banned either, and has no restrictions applied). """ return isinstance(self.participant, ( types.ChannelParticipant, types.ChatParticipant, types.ChannelParticipantSelf )) @property def is_banned(self): """ Whether the user is banned in the chat. """ return isinstance(self.participant, types.ChannelParticipantBanned) @property def has_left(self): """ Whether the user left the chat. """ return isinstance(self.participant, types.ChannelParticipantLeft) @property def add_admins(self): """ Whether the administrator can add new administrators with the same or less permissions than them. """ if not self.is_admin: return False if self.is_chat: return self.is_creator return self.participant.admin_rights.add_admins ban_users = property(**_admin_prop('ban_users', """ Whether the administrator can ban other users or not. """)) pin_messages = property(**_admin_prop('pin_messages', """ Whether the administrator can pin messages or not. """)) invite_users = property(**_admin_prop('invite_users', """ Whether the administrator can add new users to the chat. """)) delete_messages = property(**_admin_prop('delete_messages', """ Whether the administrator can delete messages from other participants. """)) edit_messages = property(**_admin_prop('edit_messages', """ Whether the administrator can edit messages. """)) post_messages = property(**_admin_prop('post_messages', """ Whether the administrator can post messages in the broadcast channel. """)) change_info = property(**_admin_prop('change_info', """ Whether the administrator can change the information about the chat, such as title or description. """)) anonymous = property(**_admin_prop('anonymous', """ Whether the administrator will remain anonymous when sending messages. """)) manage_call = property(**_admin_prop('manage_call', """ Whether the user will be able to manage group calls. """)) Telethon-1.39.0/telethon/tl/custom/qrlogin.py000066400000000000000000000101551475566265000212100ustar00rootroot00000000000000import asyncio import base64 import datetime from .. import types, functions from ... import events class QRLogin: """ QR login information. Most of the time, you will present the `url` as a QR code to the user, and while it's being shown, call `wait`. """ def __init__(self, client, ignored_ids): self._client = client self._request = functions.auth.ExportLoginTokenRequest( self._client.api_id, self._client.api_hash, ignored_ids) self._resp = None async def recreate(self): """ Generates a new token and URL for a new QR code, useful if the code has expired before it was imported. """ self._resp = await self._client(self._request) @property def token(self) -> bytes: """ The binary data representing the token. It can be used by a previously-authorized client in a call to :tl:`auth.importLoginToken` to log the client that originally requested the QR login. """ return self._resp.token @property def url(self) -> str: """ The ``tg://login`` URI with the token. When opened by a Telegram application where the user is logged in, it will import the login token. If you want to display a QR code to the user, this is the URL that should be launched when the QR code is scanned (the URL that should be contained in the QR code image you generate). Whether you generate the QR code image or not is up to you, and the library can't do this for you due to the vast ways of generating and displaying the QR code that exist. The URL simply consists of `token` base64-encoded. """ return 'tg://login?token={}'.format(base64.urlsafe_b64encode(self._resp.token).decode('utf-8').rstrip('=')) @property def expires(self) -> datetime.datetime: """ The `datetime` at which the QR code will expire. If you want to try again, you will need to call `recreate`. """ return self._resp.expires async def wait(self, timeout: float = None): """ Waits for the token to be imported by a previously-authorized client, either by scanning the QR, launching the URL directly, or calling the import method. This method **must** be called before the QR code is scanned, and must be executing while the QR code is being scanned. Otherwise, the login will not complete. Will raise `asyncio.TimeoutError` if the login doesn't complete on time. Arguments timeout (float): The timeout, in seconds, to wait before giving up. By default the library will wait until the token expires, which is often what you want. Returns On success, an instance of :tl:`User`. On failure it will raise. """ if timeout is None: timeout = (self._resp.expires - datetime.datetime.now(tz=datetime.timezone.utc)).total_seconds() event = asyncio.Event() async def handler(_update): event.set() self._client.add_event_handler(handler, events.Raw(types.UpdateLoginToken)) try: # Will raise timeout error if it doesn't complete quick enough, # which we want to let propagate await asyncio.wait_for(event.wait(), timeout=timeout) finally: self._client.remove_event_handler(handler) # We got here without it raising timeout error, so we can proceed resp = await self._client(self._request) if isinstance(resp, types.auth.LoginTokenMigrateTo): await self._client._switch_dc(resp.dc_id) resp = await self._client(functions.auth.ImportLoginTokenRequest(resp.token)) # resp should now be auth.loginTokenSuccess if isinstance(resp, types.auth.LoginTokenSuccess): user = resp.authorization.user await self._client._on_login(user) return user raise TypeError('Login token response was unexpected: {}'.format(resp)) Telethon-1.39.0/telethon/tl/custom/sendergetter.py000066400000000000000000000074161475566265000222360ustar00rootroot00000000000000import abc from ... import utils class SenderGetter(abc.ABC): """ Helper base class that introduces the `sender`, `input_sender` and `sender_id` properties and `get_sender` and `get_input_sender` methods. """ def __init__(self, sender_id=None, *, sender=None, input_sender=None): self._sender_id = sender_id self._sender = sender self._input_sender = input_sender self._client = None @property def sender(self): """ Returns the :tl:`User` or :tl:`Channel` that sent this object. It may be `None` if Telegram didn't send the sender. If you only need the ID, use `sender_id` instead. If you need to call a method which needs this chat, use `input_sender` instead. If you're using `telethon.events`, use `get_sender()` instead. """ return self._sender async def get_sender(self): """ Returns `sender`, but will make an API call to find the sender unless it's already cached. If you only need the ID, use `sender_id` instead. If you need to call a method which needs this sender, use `get_input_sender()` instead. """ # ``sender.min`` is present both in :tl:`User` and :tl:`Channel`. # It's a flag that will be set if only minimal information is # available (such as display name, but username may be missing), # in which case we want to force fetch the entire thing because # the user explicitly called a method. If the user is okay with # cached information, they may use the property instead. if (self._sender is None or getattr(self._sender, 'min', None)) \ and await self.get_input_sender(): # self.get_input_sender may refresh in which case the sender may no longer be min # However it could still incur a cost so the cheap check is done twice instead. if self._sender is None or getattr(self._sender, 'min', None): try: self._sender =\ await self._client.get_entity(self._input_sender) except ValueError: await self._refetch_sender() return self._sender @property def input_sender(self): """ This :tl:`InputPeer` is the input version of the user/channel who sent the message. Similarly to `input_chat `, this doesn't have things like username or similar, but still useful in some cases. Note that this might not be available if the library can't find the input chat, or if the message a broadcast on a channel. """ if self._input_sender is None and self._sender_id and self._client: try: self._input_sender = self._client._mb_entity_cache.get( utils.resolve_id(self._sender_id)[0])._as_input_peer() except AttributeError: pass return self._input_sender async def get_input_sender(self): """ Returns `input_sender`, but will make an API call to find the input sender unless it's already cached. """ if self.input_sender is None and self._sender_id and self._client: await self._refetch_sender() return self._input_sender @property def sender_id(self): """ Returns the marked sender integer ID, if present. If there is a sender in the object, `sender_id` will *always* be set, which is why you should use it instead of `sender.id `. """ return self._sender_id async def _refetch_sender(self): """ Re-fetches sender information through other means. """ Telethon-1.39.0/telethon/tl/patched/000077500000000000000000000000001475566265000172575ustar00rootroot00000000000000Telethon-1.39.0/telethon/tl/patched/__init__.py000066400000000000000000000010501475566265000213640ustar00rootroot00000000000000from .. import types, alltlobjects from ..custom.message import Message as _Message class MessageEmpty(_Message, types.MessageEmpty): pass types.MessageEmpty = MessageEmpty alltlobjects.tlobjects[MessageEmpty.CONSTRUCTOR_ID] = MessageEmpty class MessageService(_Message, types.MessageService): pass types.MessageService = MessageService alltlobjects.tlobjects[MessageService.CONSTRUCTOR_ID] = MessageService class Message(_Message, types.Message): pass types.Message = Message alltlobjects.tlobjects[Message.CONSTRUCTOR_ID] = Message Telethon-1.39.0/telethon/tl/tlobject.py000066400000000000000000000163361475566265000200400ustar00rootroot00000000000000import base64 import json import struct from datetime import datetime, date, timedelta, timezone import time _EPOCH_NAIVE = datetime(*time.gmtime(0)[:6]) _EPOCH_NAIVE_LOCAL = datetime(*time.localtime(0)[:6]) _EPOCH = _EPOCH_NAIVE.replace(tzinfo=timezone.utc) def _datetime_to_timestamp(dt): # If no timezone is specified, it is assumed to be in utc zone if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) # We use .total_seconds() method instead of simply dt.timestamp(), # because on Windows the latter raises OSError on datetimes ~< datetime(1970,1,1) secs = int((dt - _EPOCH).total_seconds()) # Make sure it's a valid signed 32 bit integer, as used by Telegram. # This does make very large dates wrap around, but it's the best we # can do with Telegram's limitations. return struct.unpack('i', struct.pack('I', secs & 0xffffffff))[0] def _json_default(value): if isinstance(value, bytes): return base64.b64encode(value).decode('ascii') elif isinstance(value, datetime): return value.isoformat() else: return repr(value) class TLObject: CONSTRUCTOR_ID = None SUBCLASS_OF_ID = None @staticmethod def pretty_format(obj, indent=None): """ Pretty formats the given object as a string which is returned. If indent is None, a single line will be returned. """ if indent is None: if isinstance(obj, TLObject): obj = obj.to_dict() if isinstance(obj, dict): return '{}({})'.format(obj.get('_', 'dict'), ', '.join( '{}={}'.format(k, TLObject.pretty_format(v)) for k, v in obj.items() if k != '_' )) elif isinstance(obj, str) or isinstance(obj, bytes): return repr(obj) elif hasattr(obj, '__iter__'): return '[{}]'.format( ', '.join(TLObject.pretty_format(x) for x in obj) ) else: return repr(obj) else: result = [] if isinstance(obj, TLObject): obj = obj.to_dict() if isinstance(obj, dict): result.append(obj.get('_', 'dict')) result.append('(') if obj: result.append('\n') indent += 1 for k, v in obj.items(): if k == '_': continue result.append('\t' * indent) result.append(k) result.append('=') result.append(TLObject.pretty_format(v, indent)) result.append(',\n') result.pop() # last ',\n' indent -= 1 result.append('\n') result.append('\t' * indent) result.append(')') elif isinstance(obj, str) or isinstance(obj, bytes): result.append(repr(obj)) elif hasattr(obj, '__iter__'): result.append('[\n') indent += 1 for x in obj: result.append('\t' * indent) result.append(TLObject.pretty_format(x, indent)) result.append(',\n') indent -= 1 result.append('\t' * indent) result.append(']') else: result.append(repr(obj)) return ''.join(result) @staticmethod def serialize_bytes(data): """Write bytes by using Telegram guidelines""" if not isinstance(data, bytes): if isinstance(data, str): data = data.encode('utf-8') else: raise TypeError( 'bytes or str expected, not {}'.format(type(data))) r = [] if len(data) < 254: padding = (len(data) + 1) % 4 if padding != 0: padding = 4 - padding r.append(bytes([len(data)])) r.append(data) else: padding = len(data) % 4 if padding != 0: padding = 4 - padding r.append(bytes([ 254, len(data) % 256, (len(data) >> 8) % 256, (len(data) >> 16) % 256 ])) r.append(data) r.append(bytes(padding)) return b''.join(r) @staticmethod def serialize_datetime(dt): if not dt and not isinstance(dt, timedelta): return b'\0\0\0\0' if isinstance(dt, datetime): dt = _datetime_to_timestamp(dt) elif isinstance(dt, date): dt = _datetime_to_timestamp(datetime(dt.year, dt.month, dt.day)) elif isinstance(dt, float): dt = int(dt) elif isinstance(dt, timedelta): # Timezones are tricky. datetime.utcnow() + ... timestamp() works dt = _datetime_to_timestamp(datetime.utcnow() + dt) if isinstance(dt, int): return struct.pack(' We send new access_hash for Channel with min flag since layer 102. # > Previously, we omitted it. # > That one works just to download the profile picture. # # < So, min hashes only work for getting files, # < but the non-min hash is required for any other operation? # # > Yes. # # More information: https://core.telegram.org/api/min try: if entity.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer') return entity except AttributeError: # e.g. custom.Dialog (can't cyclic import). if allow_self and hasattr(entity, 'input_entity'): return entity.input_entity elif hasattr(entity, 'entity'): return get_input_peer(entity.entity) else: _raise_cast_fail(entity, 'InputPeer') if isinstance(entity, types.User): if entity.is_self and allow_self: return types.InputPeerSelf() elif (entity.access_hash is not None and not entity.min) or not check_hash: return types.InputPeerUser(entity.id, entity.access_hash) else: raise TypeError('User without access_hash or min info cannot be input') if isinstance(entity, (types.Chat, types.ChatEmpty, types.ChatForbidden)): return types.InputPeerChat(entity.id) if isinstance(entity, types.Channel): if (entity.access_hash is not None and not entity.min) or not check_hash: return types.InputPeerChannel(entity.id, entity.access_hash) else: raise TypeError('Channel without access_hash or min info cannot be input') if isinstance(entity, types.ChannelForbidden): # "channelForbidden are never min", and since their hash is # also not optional, we assume that this truly is the case. return types.InputPeerChannel(entity.id, entity.access_hash) if isinstance(entity, types.InputUser): return types.InputPeerUser(entity.user_id, entity.access_hash) if isinstance(entity, types.InputChannel): return types.InputPeerChannel(entity.channel_id, entity.access_hash) if isinstance(entity, types.InputUserSelf): return types.InputPeerSelf() if isinstance(entity, types.InputUserFromMessage): return types.InputPeerUserFromMessage(entity.peer, entity.msg_id, entity.user_id) if isinstance(entity, types.InputChannelFromMessage): return types.InputPeerChannelFromMessage(entity.peer, entity.msg_id, entity.channel_id) if isinstance(entity, types.UserEmpty): return types.InputPeerEmpty() if isinstance(entity, types.UserFull): return get_input_peer(entity.user) if isinstance(entity, types.ChatFull): return types.InputPeerChat(entity.id) if isinstance(entity, types.PeerChat): return types.InputPeerChat(entity.chat_id) _raise_cast_fail(entity, 'InputPeer') def get_input_channel(entity): """ Similar to :meth:`get_input_peer`, but for :tl:`InputChannel`'s alone. .. important:: This method does not validate for invalid general-purpose access hashes, unlike `get_input_peer`. Consider using instead: ``get_input_channel(get_input_peer(channel))``. """ try: if entity.SUBCLASS_OF_ID == 0x40f202fd: # crc32(b'InputChannel') return entity except AttributeError: _raise_cast_fail(entity, 'InputChannel') if isinstance(entity, (types.Channel, types.ChannelForbidden)): return types.InputChannel(entity.id, entity.access_hash or 0) if isinstance(entity, types.InputPeerChannel): return types.InputChannel(entity.channel_id, entity.access_hash) if isinstance(entity, types.InputPeerChannelFromMessage): return types.InputChannelFromMessage(entity.peer, entity.msg_id, entity.channel_id) _raise_cast_fail(entity, 'InputChannel') def get_input_user(entity): """ Similar to :meth:`get_input_peer`, but for :tl:`InputUser`'s alone. .. important:: This method does not validate for invalid general-purpose access hashes, unlike `get_input_peer`. Consider using instead: ``get_input_channel(get_input_peer(channel))``. """ try: if entity.SUBCLASS_OF_ID == 0xe669bf46: # crc32(b'InputUser'): return entity except AttributeError: _raise_cast_fail(entity, 'InputUser') if isinstance(entity, types.User): if entity.is_self: return types.InputUserSelf() else: return types.InputUser(entity.id, entity.access_hash or 0) if isinstance(entity, types.InputPeerSelf): return types.InputUserSelf() if isinstance(entity, (types.UserEmpty, types.InputPeerEmpty)): return types.InputUserEmpty() if isinstance(entity, types.UserFull): return get_input_user(entity.user) if isinstance(entity, types.InputPeerUser): return types.InputUser(entity.user_id, entity.access_hash) if isinstance(entity, types.InputPeerUserFromMessage): return types.InputUserFromMessage(entity.peer, entity.msg_id, entity.user_id) _raise_cast_fail(entity, 'InputUser') def get_input_dialog(dialog): """Similar to :meth:`get_input_peer`, but for dialogs""" try: if dialog.SUBCLASS_OF_ID == 0xa21c9795: # crc32(b'InputDialogPeer') return dialog if dialog.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer') return types.InputDialogPeer(dialog) except AttributeError: _raise_cast_fail(dialog, 'InputDialogPeer') try: return types.InputDialogPeer(get_input_peer(dialog)) except TypeError: pass _raise_cast_fail(dialog, 'InputDialogPeer') def get_input_document(document): """Similar to :meth:`get_input_peer`, but for documents""" try: if document.SUBCLASS_OF_ID == 0xf33fdb68: # crc32(b'InputDocument'): return document except AttributeError: _raise_cast_fail(document, 'InputDocument') if isinstance(document, types.Document): return types.InputDocument( id=document.id, access_hash=document.access_hash, file_reference=document.file_reference) if isinstance(document, types.DocumentEmpty): return types.InputDocumentEmpty() if isinstance(document, types.MessageMediaDocument): return get_input_document(document.document) if isinstance(document, types.Message): return get_input_document(document.media) _raise_cast_fail(document, 'InputDocument') def get_input_photo(photo): """Similar to :meth:`get_input_peer`, but for photos""" try: if photo.SUBCLASS_OF_ID == 0x846363e0: # crc32(b'InputPhoto'): return photo except AttributeError: _raise_cast_fail(photo, 'InputPhoto') if isinstance(photo, types.Message): photo = photo.media if isinstance(photo, (types.photos.Photo, types.MessageMediaPhoto)): photo = photo.photo if isinstance(photo, types.Photo): return types.InputPhoto(id=photo.id, access_hash=photo.access_hash, file_reference=photo.file_reference) if isinstance(photo, types.PhotoEmpty): return types.InputPhotoEmpty() if isinstance(photo, types.messages.ChatFull): photo = photo.full_chat if isinstance(photo, types.ChannelFull): return get_input_photo(photo.chat_photo) elif isinstance(photo, types.UserFull): return get_input_photo(photo.profile_photo) elif isinstance(photo, (types.Channel, types.Chat, types.User)): return get_input_photo(photo.photo) if isinstance(photo, (types.UserEmpty, types.ChatEmpty, types.ChatForbidden, types.ChannelForbidden)): return types.InputPhotoEmpty() _raise_cast_fail(photo, 'InputPhoto') def get_input_chat_photo(photo): """Similar to :meth:`get_input_peer`, but for chat photos""" try: if photo.SUBCLASS_OF_ID == 0xd4eb2d74: # crc32(b'InputChatPhoto') return photo elif photo.SUBCLASS_OF_ID == 0xe7655f1f: # crc32(b'InputFile'): return types.InputChatUploadedPhoto(photo) except AttributeError: _raise_cast_fail(photo, 'InputChatPhoto') photo = get_input_photo(photo) if isinstance(photo, types.InputPhoto): return types.InputChatPhoto(photo) elif isinstance(photo, types.InputPhotoEmpty): return types.InputChatPhotoEmpty() _raise_cast_fail(photo, 'InputChatPhoto') def get_input_geo(geo): """Similar to :meth:`get_input_peer`, but for geo points""" try: if geo.SUBCLASS_OF_ID == 0x430d225: # crc32(b'InputGeoPoint'): return geo except AttributeError: _raise_cast_fail(geo, 'InputGeoPoint') if isinstance(geo, types.GeoPoint): return types.InputGeoPoint(lat=geo.lat, long=geo.long) if isinstance(geo, types.GeoPointEmpty): return types.InputGeoPointEmpty() if isinstance(geo, types.MessageMediaGeo): return get_input_geo(geo.geo) if isinstance(geo, types.Message): return get_input_geo(geo.media) _raise_cast_fail(geo, 'InputGeoPoint') def get_input_media( media, *, is_photo=False, attributes=None, force_document=False, voice_note=False, video_note=False, supports_streaming=False, ttl=None ): """ Similar to :meth:`get_input_peer`, but for media. If the media is :tl:`InputFile` and ``is_photo`` is known to be `True`, it will be treated as an :tl:`InputMediaUploadedPhoto`. Else, the rest of parameters will indicate how to treat it. """ try: if media.SUBCLASS_OF_ID == 0xfaf846f4: # crc32(b'InputMedia') return media elif media.SUBCLASS_OF_ID == 0x846363e0: # crc32(b'InputPhoto') return types.InputMediaPhoto(media, ttl_seconds=ttl) elif media.SUBCLASS_OF_ID == 0xf33fdb68: # crc32(b'InputDocument') return types.InputMediaDocument(media, ttl_seconds=ttl) except AttributeError: _raise_cast_fail(media, 'InputMedia') if isinstance(media, types.MessageMediaPhoto): return types.InputMediaPhoto( id=get_input_photo(media.photo), ttl_seconds=ttl or media.ttl_seconds ) if isinstance(media, (types.Photo, types.photos.Photo, types.PhotoEmpty)): return types.InputMediaPhoto( id=get_input_photo(media), ttl_seconds=ttl ) if isinstance(media, types.MessageMediaDocument): return types.InputMediaDocument( id=get_input_document(media.document), ttl_seconds=ttl or media.ttl_seconds ) if isinstance(media, (types.Document, types.DocumentEmpty)): return types.InputMediaDocument( id=get_input_document(media), ttl_seconds=ttl ) if isinstance(media, (types.InputFile, types.InputFileBig)): if is_photo: return types.InputMediaUploadedPhoto(file=media, ttl_seconds=ttl) else: attrs, mime = get_attributes( media, attributes=attributes, force_document=force_document, voice_note=voice_note, video_note=video_note, supports_streaming=supports_streaming ) return types.InputMediaUploadedDocument( file=media, mime_type=mime, attributes=attrs, force_file=force_document, ttl_seconds=ttl) if isinstance(media, types.MessageMediaGame): return types.InputMediaGame(id=types.InputGameID( id=media.game.id, access_hash=media.game.access_hash )) if isinstance(media, types.MessageMediaContact): return types.InputMediaContact( phone_number=media.phone_number, first_name=media.first_name, last_name=media.last_name, vcard='' ) if isinstance(media, types.MessageMediaGeo): return types.InputMediaGeoPoint(geo_point=get_input_geo(media.geo)) if isinstance(media, types.MessageMediaVenue): return types.InputMediaVenue( geo_point=get_input_geo(media.geo), title=media.title, address=media.address, provider=media.provider, venue_id=media.venue_id, venue_type='' ) if isinstance(media, types.MessageMediaDice): return types.InputMediaDice(media.emoticon) if isinstance(media, ( types.MessageMediaEmpty, types.MessageMediaUnsupported, types.ChatPhotoEmpty, types.UserProfilePhotoEmpty, types.ChatPhoto, types.UserProfilePhoto)): return types.InputMediaEmpty() if isinstance(media, types.Message): return get_input_media(media.media, is_photo=is_photo, ttl=ttl) if isinstance(media, types.MessageMediaPoll): if media.poll.quiz: if not media.results.results: # A quiz has correct answers, which we don't know until answered. # If the quiz hasn't been answered we can't reconstruct it properly. raise TypeError('Cannot cast unanswered quiz to any kind of InputMedia.') correct_answers = [r.option for r in media.results.results if r.correct] else: correct_answers = None return types.InputMediaPoll( poll=media.poll, correct_answers=correct_answers, solution=media.results.solution, solution_entities=media.results.solution_entities, ) if isinstance(media, types.Poll): return types.InputMediaPoll(media) _raise_cast_fail(media, 'InputMedia') def get_input_message(message): """Similar to :meth:`get_input_peer`, but for input messages.""" try: if isinstance(message, int): # This case is really common too return types.InputMessageID(message) elif message.SUBCLASS_OF_ID == 0x54b6bcc5: # crc32(b'InputMessage'): return message elif message.SUBCLASS_OF_ID == 0x790009e3: # crc32(b'Message'): return types.InputMessageID(message.id) except AttributeError: pass _raise_cast_fail(message, 'InputMedia') def get_input_group_call(call): """Similar to :meth:`get_input_peer`, but for input calls.""" try: if call.SUBCLASS_OF_ID == 0x58611ab1: # crc32(b'InputGroupCall') return call elif call.SUBCLASS_OF_ID == 0x20b4f320: # crc32(b'GroupCall') return types.InputGroupCall(id=call.id, access_hash=call.access_hash) except AttributeError: _raise_cast_fail(call, 'InputGroupCall') def _get_entity_pair(entity_id, entities, cache, get_input_peer=get_input_peer): """ Returns ``(entity, input_entity)`` for the given entity ID. """ if not entity_id: return None, None entity = entities.get(entity_id) try: input_entity = cache.get(resolve_id(entity_id)[0])._as_input_peer() except AttributeError: # AttributeError is unlikely, so another TypeError won't hurt try: input_entity = get_input_peer(entity) except TypeError: input_entity = None return entity, input_entity def get_message_id(message): """Similar to :meth:`get_input_peer`, but for message IDs.""" if message is None: return None if isinstance(message, int): return message if isinstance(message, types.InputMessageID): return message.id try: if message.SUBCLASS_OF_ID == 0x790009e3: # hex(crc32(b'Message')) = 0x790009e3 return message.id except AttributeError: pass raise TypeError('Invalid message type: {}'.format(type(message))) def _get_metadata(file): if not hachoir: return stream = None close_stream = True seekable = True # The parser may fail and we don't want to crash if # the extraction process fails. try: # Note: aiofiles are intentionally left out for simplicity. # `helpers._FileStream` is async only for simplicity too, so can't # reuse it here. if isinstance(file, str): stream = open(file, 'rb') elif isinstance(file, bytes): stream = io.BytesIO(file) else: stream = file close_stream = False if getattr(file, 'seekable', None): seekable = file.seekable() else: seekable = False if not seekable: return None pos = stream.tell() filename = getattr(file, 'name', '') parser = hachoir.parser.guess.guessParser(hachoir.stream.InputIOStream( stream, source='file:' + filename, tags=[], filename=filename )) return hachoir.metadata.extractMetadata(parser) except Exception as e: _log.warning('Failed to analyze %s: %s %s', file, e.__class__, e) finally: if stream and close_stream: stream.close() elif stream and seekable: stream.seek(pos) def get_attributes(file, *, attributes=None, mime_type=None, force_document=False, voice_note=False, video_note=False, supports_streaming=False, thumb=None): """ Get a list of attributes for the given file and the mime type as a tuple ([attribute], mime_type). """ # Note: ``file.name`` works for :tl:`InputFile` and some `IOBase` streams name = file if isinstance(file, str) else getattr(file, 'name', 'unnamed') if mime_type is None: mime_type = mimetypes.guess_type(name)[0] attr_dict = {types.DocumentAttributeFilename: types.DocumentAttributeFilename(os.path.basename(name))} if is_audio(file): m = _get_metadata(file) if m: if m.has('author'): performer = m.get('author') elif m.has('artist'): performer = m.get('artist') else: performer = None attr_dict[types.DocumentAttributeAudio] = \ types.DocumentAttributeAudio( voice=voice_note, title=m.get('title') if m.has('title') else None, performer=performer, duration=int(m.get('duration').seconds if m.has('duration') else 0) ) if not force_document and is_video(file): m = _get_metadata(file) if m: doc = types.DocumentAttributeVideo( round_message=video_note, w=m.get('width') if m.has('width') else 1, h=m.get('height') if m.has('height') else 1, duration=int(m.get('duration').seconds if m.has('duration') else 1), supports_streaming=supports_streaming ) elif thumb: t_m = _get_metadata(thumb) width = 1 height = 1 if t_m and t_m.has("width"): width = t_m.get("width") if t_m and t_m.has("height"): height = t_m.get("height") doc = types.DocumentAttributeVideo( 0, width, height, round_message=video_note, supports_streaming=supports_streaming) else: doc = types.DocumentAttributeVideo( 0, 1, 1, round_message=video_note, supports_streaming=supports_streaming) attr_dict[types.DocumentAttributeVideo] = doc if voice_note: if types.DocumentAttributeAudio in attr_dict: attr_dict[types.DocumentAttributeAudio].voice = True else: attr_dict[types.DocumentAttributeAudio] = \ types.DocumentAttributeAudio(0, voice=True) # Now override the attributes if any. As we have a dict of # {cls: instance}, we can override any class with the list # of attributes provided by the user easily. if attributes: for a in attributes: attr_dict[type(a)] = a # Ensure we have a mime type, any; but it cannot be None # 'The "octet-stream" subtype is used to indicate that a body # contains arbitrary binary data.' if not mime_type: mime_type = 'application/octet-stream' return list(attr_dict.values()), mime_type def sanitize_parse_mode(mode): """ Converts the given parse mode into an object with ``parse`` and ``unparse`` callable properties. """ if not mode: return None if (all(hasattr(mode, x) for x in ('parse', 'unparse')) and all(callable(x) for x in (mode.parse, mode.unparse))): return mode elif callable(mode): class CustomMode: @staticmethod def unparse(text, entities): raise NotImplementedError CustomMode.parse = mode return CustomMode elif isinstance(mode, str): try: return { 'md': markdown, 'markdown': markdown, 'htm': html, 'html': html }[mode.lower()] except KeyError: raise ValueError('Unknown parse mode {}'.format(mode)) else: raise TypeError('Invalid parse mode type {}'.format(mode)) def get_input_location(location): """ Similar to :meth:`get_input_peer`, but for input messages. Note that this returns a tuple ``(dc_id, location)``, the ``dc_id`` being present if known. """ info = _get_file_info(location) return info.dc_id, info.location def _get_file_info(location): try: if location.SUBCLASS_OF_ID == 0x1523d462: return _FileInfo(None, location, None) # crc32(b'InputFileLocation'): except AttributeError: _raise_cast_fail(location, 'InputFileLocation') if isinstance(location, types.Message): location = location.media if isinstance(location, types.MessageMediaDocument): location = location.document elif isinstance(location, types.MessageMediaPhoto): location = location.photo if isinstance(location, types.Document): return _FileInfo(location.dc_id, types.InputDocumentFileLocation( id=location.id, access_hash=location.access_hash, file_reference=location.file_reference, thumb_size='' # Presumably to download one of its thumbnails ), location.size) elif isinstance(location, types.Photo): return _FileInfo(location.dc_id, types.InputPhotoFileLocation( id=location.id, access_hash=location.access_hash, file_reference=location.file_reference, thumb_size=location.sizes[-1].type ), _photo_size_byte_count(location.sizes[-1])) _raise_cast_fail(location, 'InputFileLocation') def _get_extension(file): """ Gets the extension for the given file, which can be either a str or an ``open()``'ed file (which has a ``.name`` attribute). """ if isinstance(file, str): return os.path.splitext(file)[-1] elif isinstance(file, pathlib.Path): return file.suffix elif getattr(file, 'name', None): # Note: ``file.name`` works for :tl:`InputFile` and some `IOBase` return _get_extension(file.name) else: # Maybe it's a Telegram media return get_extension(file) def is_image(file): """ Returns `True` if the file extension looks like an image file to Telegram. """ match = re.match(r'\.(png|jpe?g)', _get_extension(file), re.IGNORECASE) if match: return True else: return isinstance(resolve_bot_file_id(file), types.Photo) def is_gif(file): """ Returns `True` if the file extension looks like a gif file to Telegram. """ return re.match(r'\.gif', _get_extension(file), re.IGNORECASE) def is_audio(file): """Returns `True` if the file has an audio mime type.""" ext = _get_extension(file) if not ext: metadata = _get_metadata(file) if metadata and metadata.has('mime_type'): return metadata.get('mime_type').startswith('audio/') else: return False else: file = 'a' + ext return (mimetypes.guess_type(file)[0] or '').startswith('audio/') def is_video(file): """Returns `True` if the file has a video mime type.""" ext = _get_extension(file) if not ext: metadata = _get_metadata(file) if metadata and metadata.has('mime_type'): return metadata.get('mime_type').startswith('video/') else: return False else: file = 'a' + ext return (mimetypes.guess_type(file)[0] or '').startswith('video/') def is_list_like(obj): """ Returns `True` if the given object looks like a list. Checking ``if hasattr(obj, '__iter__')`` and ignoring ``str/bytes`` is not enough. Things like ``open()`` are also iterable (and probably many other things), so just support the commonly known list-like objects. """ return isinstance(obj, (list, tuple, set, dict, range, GeneratorType)) def parse_phone(phone): """Parses the given phone, or returns `None` if it's invalid.""" if isinstance(phone, int): return str(phone) else: phone = re.sub(r'[+()\s-]', '', str(phone)) if phone.isdigit(): return phone def parse_username(username): """ Parses the given username or channel access hash, given a string, username or URL. Returns a tuple consisting of both the stripped, lowercase username and whether it is a joinchat/ hash (in which case is not lowercase'd). Returns ``(None, False)`` if the ``username`` or link is not valid. """ username = username.strip() m = USERNAME_RE.match(username) or TG_JOIN_RE.match(username) if m: username = username[m.end():] is_invite = bool(m.group(1)) if is_invite: return username, True else: username = username.rstrip('/') if VALID_USERNAME_RE.match(username): return username.lower(), False else: return None, False def get_inner_text(text, entities): """ Gets the inner text that's surrounded by the given entities. For instance: text = 'hey!', entity = MessageEntityBold(2, 2) -> 'y!'. :param text: the original text. :param entities: the entity or entities that must be matched. :return: a single result or a list of the text surrounded by the entities. """ text = add_surrogate(text) result = [] for e in entities: start = e.offset end = e.offset + e.length result.append(del_surrogate(text[start:end])) return result def get_peer(peer): try: if isinstance(peer, int): pid, cls = resolve_id(peer) return cls(pid) elif peer.SUBCLASS_OF_ID == 0x2d45687: return peer elif isinstance(peer, ( types.contacts.ResolvedPeer, types.InputNotifyPeer, types.TopPeer, types.Dialog, types.DialogPeer)): return peer.peer elif isinstance(peer, types.ChannelFull): return types.PeerChannel(peer.id) elif isinstance(peer, types.UserEmpty): return types.PeerUser(peer.id) elif isinstance(peer, types.ChatEmpty): return types.PeerChat(peer.id) if peer.SUBCLASS_OF_ID in (0x7d7c6f86, 0xd9c7fc18): # ChatParticipant, ChannelParticipant return types.PeerUser(peer.user_id) peer = get_input_peer(peer, allow_self=False, check_hash=False) if isinstance(peer, (types.InputPeerUser, types.InputPeerUserFromMessage)): return types.PeerUser(peer.user_id) elif isinstance(peer, types.InputPeerChat): return types.PeerChat(peer.chat_id) elif isinstance(peer, (types.InputPeerChannel, types.InputPeerChannelFromMessage)): return types.PeerChannel(peer.channel_id) except (AttributeError, TypeError): pass _raise_cast_fail(peer, 'Peer') def get_peer_id(peer, add_mark=True): """ Convert the given peer into its marked ID by default. This "mark" comes from the "bot api" format, and with it the peer type can be identified back. User ID is left unmodified, chat ID is negated, and channel ID is "prefixed" with -100: * ``user_id`` * ``-chat_id`` * ``-100channel_id`` The original ID and the peer type class can be returned with a call to :meth:`resolve_id(marked_id)`. """ # First we assert it's a Peer TLObject, or early return for integers if isinstance(peer, int): return peer if add_mark else resolve_id(peer)[0] # Tell the user to use their client to resolve InputPeerSelf if we got one if isinstance(peer, types.InputPeerSelf): _raise_cast_fail(peer, 'int (you might want to use client.get_peer_id)') try: peer = get_peer(peer) except TypeError: _raise_cast_fail(peer, 'int') if isinstance(peer, types.PeerUser): return peer.user_id elif isinstance(peer, types.PeerChat): # Check in case the user mixed things up to avoid blowing up if not (0 < peer.chat_id <= 9999999999): peer.chat_id = resolve_id(peer.chat_id)[0] return -peer.chat_id if add_mark else peer.chat_id else: # if isinstance(peer, types.PeerChannel): # Check in case the user mixed things up to avoid blowing up if not (0 < peer.channel_id <= 9999999999): peer.channel_id = resolve_id(peer.channel_id)[0] if not add_mark: return peer.channel_id # Growing backwards from -100_0000_000_000 indicates it's a channel return -(1000000000000 + peer.channel_id) def resolve_id(marked_id): """Given a marked ID, returns the original ID and its :tl:`Peer` type.""" if marked_id >= 0: return marked_id, types.PeerUser marked_id = -marked_id if marked_id > 1000000000000: marked_id -= 1000000000000 return marked_id, types.PeerChannel else: return marked_id, types.PeerChat def _rle_decode(data): """ Decodes run-length-encoded `data`. """ if not data: return data new = b'' last = b'' for cur in data: if last == b'\0': new += last * cur last = b'' else: new += last last = bytes([cur]) return new + last def _rle_encode(string): new = b'' count = 0 for cur in string: if not cur: count += 1 else: if count: new += b'\0' + bytes([count]) count = 0 new += bytes([cur]) return new def _decode_telegram_base64(string): """ Decodes a url-safe base64-encoded string into its bytes by first adding the stripped necessary padding characters. This is the way Telegram shares binary data as strings, such as Bot API-style file IDs or invite links. Returns `None` if the input string was not valid. """ try: return base64.urlsafe_b64decode(string + '=' * (len(string) % 4)) except (binascii.Error, ValueError, TypeError): return None # not valid base64, not valid ascii, not a string def _encode_telegram_base64(string): """ Inverse for `_decode_telegram_base64`. """ try: return base64.urlsafe_b64encode(string).rstrip(b'=').decode('ascii') except (binascii.Error, ValueError, TypeError): return None # not valid base64, not valid ascii, not a string def resolve_bot_file_id(file_id): """ Given a Bot API-style `file_id `, returns the media it represents. If the `file_id ` is not valid, `None` is returned instead. Note that the `file_id ` does not have information such as image dimensions or file size, so these will be zero if present. For thumbnails, the photo ID and hash will always be zero. """ data = _rle_decode(_decode_telegram_base64(file_id)) if not data: return None # This isn't officially documented anywhere, but # we assume the last byte is some kind of "version". data, version = data[:-1], data[-1] if version not in (2, 4): return None if (version == 2 and len(data) == 24) or (version == 4 and len(data) == 25): if version == 2: file_type, dc_id, media_id, access_hash = struct.unpack('LQ', payload)) elif len(payload) == 16: return struct.unpack('>LLQ', payload) else: pass except (struct.error, TypeError): pass return None, None, None def resolve_inline_message_id(inline_msg_id): """ Resolves an inline message ID. Returns a tuple of ``(message id, peer, dc id, access hash)`` The ``peer`` may either be a :tl:`PeerUser` referencing the user who sent the message via the bot in a private conversation or small group chat, or a :tl:`PeerChannel` if the message was sent in a channel. The ``access_hash`` does not have any use yet. """ try: dc_id, message_id, pid, access_hash = \ struct.unpack('> bit_shift) & 0b00011111 byte_index, bit_shift = divmod(value_count - 1, 8) if byte_index == len(waveform) - 1: value = waveform[byte_index] else: value = struct.unpack('> bit_shift) & 0b00011111 return bytes(result) def split_text(text, entities, *, limit=4096, max_entities=100, split_at=(r'\n', r'\s', '.')): """ Split a message text and entities into multiple messages, each with their own set of entities. This allows sending a very large message as multiple messages while respecting the formatting. Arguments text (`str`): The message text. entities (List[:tl:`MessageEntity`]) The formatting entities. limit (`int`): The maximum message length of each individual message. max_entities (`int`): The maximum amount of entities that will be present in each individual message. split_at (Tuplel[`str`]): The list of regular expressions that will determine where to split the text. By default, a newline is searched. If no newline is present, a space is searched. If no space is found, the split will be made at any character. The last expression should always match a character, or else the text will stop being splitted and the resulting text may be larger than the limit. Yields Pairs of ``(str, entities)`` with the split message. Example .. code-block:: python from telethon import utils from telethon.extensions import markdown very_long_markdown_text = "..." text, entities = markdown.parse(very_long_markdown_text) for text, entities in utils.split_text(text, entities): await client.send_message(chat, text, formatting_entities=entities) """ # TODO add test cases (multiple entities beyond cutoff, at cutoff, splitting at emoji) # TODO try to optimize this a bit more? (avoid new_ent, smarter update method) def update(ent, **updates): kwargs = ent.to_dict() del kwargs['_'] kwargs.update(updates) return ent.__class__(**kwargs) text = add_surrogate(text) split_at = tuple(map(re.compile, split_at)) while True: if len(entities) > max_entities: last_ent = entities[max_entities - 1] cur_limit = min(limit, last_ent.offset + last_ent.length) else: cur_limit = limit if len(text) <= cur_limit: break for split in split_at: for i in reversed(range(cur_limit)): m = split.match(text, pos=i) if m: cur_text, new_text = text[:m.end()], text[m.end():] cur_ent, new_ent = [], [] for ent in entities: if ent.offset < m.end(): if ent.offset + ent.length > m.end(): cur_ent.append(update(ent, length=m.end() - ent.offset)) new_ent.append(update(ent, offset=0, length=ent.offset + ent.length - m.end())) else: cur_ent.append(ent) else: new_ent.append(update(ent, offset=ent.offset - m.end())) yield del_surrogate(cur_text), cur_ent text, entities = new_text, new_ent break else: continue break else: # Can't find where to split, just return the remaining text and entities break yield del_surrogate(text), entities class AsyncClassWrapper: def __init__(self, wrapped): self.wrapped = wrapped def __getattr__(self, item): w = getattr(self.wrapped, item) async def wrapper(*args, **kwargs): val = w(*args, **kwargs) return await val if inspect.isawaitable(val) else val if callable(w): return wrapper else: return w def stripped_photo_to_jpg(stripped): """ Adds the JPG header and footer to a stripped image. Ported from https://github.com/telegramdesktop/tdesktop/blob/bec39d89e19670eb436dc794a8f20b657cb87c71/Telegram/SourceFiles/ui/image/image.cpp#L225 """ # NOTE: Changes here should update _photo_size_byte_count if len(stripped) < 3 or stripped[0] != 1: return stripped header = bytearray(b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00\xff\xdb\x00C\x00(\x1c\x1e#\x1e\x19(#!#-+(0 Telethon-1.39.0/telethon_examples/README.md000066400000000000000000000160031475566265000204050ustar00rootroot00000000000000# Examples This folder contains several single-file examples using [Telethon]. ## Requisites You should have the `telethon` library installed with `pip`. Run `python3 -m pip install --upgrade telethon --user` if you don't have it installed yet (this is the most portable way to install it). The scripts will ask you for your API ID, hash, etc. through standard input. You can also define the following environment variables to avoid doing so: * `TG_API_ID`, this is your API ID from https://my.telegram.org. * `TG_API_HASH`, this is your API hash from https://my.telegram.org. * `TG_TOKEN`, this is your bot token from [@BotFather] for bot examples. * `TG_SESSION`, this is the name of the `*.session` file to use. ## Downloading Examples You may download all and run any example by typing in a terminal: ```sh git clone https://github.com/LonamiWebs/Telethon.git cd Telethon cd telethon_examples python3 gui.py ``` You can also right-click the title of any example and use "Save Link As…" to download only a particular example. All examples are licensed under the [CC0 License], so you can use them as the base for your own code without worrying about copyright. ## Available Examples ### [`print_updates.py`] * Usable as: **user and bot**. * Difficulty: **easy**. Trivial example that just prints all the updates Telegram originally sends. Your terminal should support UTF-8, or Python may fail to print some characters on screen. ### [`print_messages.py`] * Usable as: **user and bot**. * Difficulty: **easy**. This example uses the different `@client.on` syntax to register event handlers, and uses the `pattern=` variable to filter only some messages. There are a lot other things you can do, but you should refer to the documentation of [`events.NewMessage`] since this is only a simple example. ### [`replier.py`] * Usable as: **user and bot**. * Difficulty: **easy**. This example showcases a third way to add event handlers (using decorators but without the client; you should use the one you prefer) and will also reply to some messages with different reactions, or to your commands. It also shows how to enable `logging`, which you should always do, but was not really needed for the previous two trivial examples. ### [`assistant.py`] * Usable as a: **bot**. * Difficulty: **medium**. This example is the core of the actual bot account [@TelethonianBot] running in the [official Telethon's chat] to help people out. It showcases how to create an extremely simple "plugins" system with Telethon, but you're free to borrow ideas from it and make it as fancy as you like (perhaps you want to add hot reloading?). The plugins are a separate Python file each which get loaded dynamically and can be found at . To use them, clone the repository into a `plugins` folder next to `assistant.py` and then run `assistant.py`. The content of the plugins or how they work is not really relevant. You can disable them by moving them elsewhere or deleting the file entirely. The point is to learn how you can build fancy things with your own code and Telethon. ### [`interactive_telegram_client.py`] * Usable as: **user**. * Difficulty: **medium**. Interactive terminal client that you can use to list your dialogs, send messages, delete them, and download media. The code is a bit long which may make it harder to follow, and requires saving some state in order for downloads to work later. ### [`quart_login.py`] * Usable as: **user**. * Difficulty: **medium**. Web-based application using [Quart](https://pgjones.gitlab.io/quart/index.html) (an `asyncio` alternative to [Flask](http://flask.pocoo.org/)) and Telethon together. The example should work as a base for Quart applications *with a single global client*, and it should be easy to adapt for multiple clients by following the comments in the code. It showcases how to login manually (ask for phone, code, and login), and once the user is logged in, some messages and photos will be shown in the page. There is nothing special about Quart. It was chosen because it's a drop-in replacement for Flask, the most popular option for web-apps. You can use any `asyncio` library with Telethon just as well, like [Sanic](https://sanic.readthedocs.io/en/latest/index.html) or [aiohttp](https://docs.aiohttp.org/en/stable/). You can even use Flask, if you learn how to use `threading` and `asyncio` together. ### [`gui.py`] * Usable as: **user and bot**. * Difficulty: **high**. This is a simple GUI written with [`tkinter`] which becomes more complicated when there's a need to use [`asyncio`] (although it's only a bit of additional setup). The code to deal with the interface and the commands the GUI supports also complicate the code further and require knowledge and careful reading. This example is the actual bot account [@TelethonianBot] running in the [official Telethon's chat] to help people out. The file is a bit big and assumes some [`asyncio`] knowledge, but otherwise is easy to follow. ![Screenshot of the tkinter GUI][tkinter GUI] ### [`payment.py`](https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/payment.py) * Usable as: **bot**. * Difficulty: **medium**. This example shows how to make invoices (Telegram's way of requesting payments) via a bot account. The example does not include how to add shipping information, though. You'll need to obtain a "provider token" to use this example, so please read [Telegram's guide on payments](https://core.telegram.org/bots/payments) before using this example. It makes use of the ["raw API"](https://tl.telethon.dev) (that is, no friendly `client.` methods), which can be helpful in understanding how it works and how it can be used. [Telethon]: https://github.com/LonamiWebs/Telethon [CC0 License]: https://github.com/LonamiWebs/Telethon/blob/v1/telethon_examples/LICENSE [@BotFather]: https://t.me/BotFather [`assistant.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/assistant.py [`quart_login.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/quart_login.py [`gui.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/gui.py [`interactive_telegram_client.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/interactive_telegram_client.py [`print_messages.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/print_messages.py [`print_updates.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/print_updates.py [`replier.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/replier.py [@TelethonianBot]: https://t.me/TelethonianBot [official Telethon's chat]: https://t.me/TelethonChat [`asyncio`]: https://docs.python.org/3/library/asyncio.html [`tkinter`]: https://docs.python.org/3/library/tkinter.html [tkinter GUI]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/screenshot-gui.jpg [`events.NewMessage`]: https://docs.telethon.dev/en/stable/modules/events.html#telethon.events.newmessage.NewMessage Telethon-1.39.0/telethon_examples/assistant.py000066400000000000000000000033101475566265000215060ustar00rootroot00000000000000""" This file is only the "core" of the bot. It is responsible for loading the plugins module and initializing it. You may obtain the plugins by running: git clone https://github.com/Lonami/TelethonianBotExt plugins In the same folder where this file lives. As a result, the directory should look like the following: assistant.py plugins/ ... """ import asyncio import os import sys import time from telethon import TelegramClient try: # Standalone script assistant.py with folder plugins/ import plugins except ImportError: try: # Running as a module with `python -m assistant` and structure: # # assistant/ # __main__.py (this file) # plugins/ (cloned) from . import plugins except ImportError: print('could not load the plugins module, does the directory exist ' 'in the correct location?', file=sys.stderr) exit(1) def get_env(name, message, cast=str): if name in os.environ: return os.environ[name] while True: value = input(message) try: return cast(value) except ValueError as e: print(e, file=sys.stderr) time.sleep(1) API_ID = get_env('TG_API_ID', 'Enter your API ID: ', int) API_HASH = get_env('TG_API_HASH', 'Enter your API hash: ') TOKEN = get_env('TG_TOKEN', 'Enter the bot token: ') NAME = TOKEN.split(':')[0] async def main(): bot = TelegramClient(NAME, API_ID, API_HASH) await bot.start(bot_token=TOKEN) try: await plugins.init(bot) await bot.run_until_disconnected() finally: await bot.disconnect() if __name__ == '__main__': asyncio.run(main()) Telethon-1.39.0/telethon_examples/gui.py000066400000000000000000000314531475566265000202720ustar00rootroot00000000000000import asyncio import collections import functools import inspect import os import re import sys import time import tkinter import tkinter.constants import tkinter.scrolledtext import tkinter.ttk from telethon import TelegramClient, events, utils # Some configuration for the app TITLE = 'Telethon GUI' SIZE = '640x280' REPLY = re.compile(r'\.r\s*(\d+)\s*(.+)', re.IGNORECASE) DELETE = re.compile(r'\.d\s*(\d+)', re.IGNORECASE) EDIT = re.compile(r'\.s(.+?[^\\])/(.*)', re.IGNORECASE) def get_env(name, message, cast=str): if name in os.environ: return os.environ[name] while True: value = input(message) try: return cast(value) except ValueError as e: print(e, file=sys.stderr) time.sleep(1) # Session name, API ID and hash to use; loaded from environmental variables SESSION = os.environ.get('TG_SESSION', 'gui') API_ID = get_env('TG_API_ID', 'Enter your API ID: ', int) API_HASH = get_env('TG_API_HASH', 'Enter your API hash: ') def sanitize_str(string): return ''.join(x if ord(x) <= 0xffff else '{{{:x}ū}}'.format(ord(x)) for x in string) def callback(func): """ This decorator turns `func` into a callback for Tkinter to be able to use, even if `func` is an awaitable coroutine. """ @functools.wraps(func) def wrapped(*args, **kwargs): result = func(*args, **kwargs) if inspect.iscoroutine(result): asyncio.create_task(result) return wrapped def allow_copy(widget): """ This helper makes `widget` readonly but allows copying with ``Ctrl+C``. """ widget.bind('', lambda e: None) widget.bind('', lambda e: "break") class App(tkinter.Tk): """ Our main GUI application; we subclass `tkinter.Tk` so the `self` instance can be the root widget. One must be careful when assigning members or defining methods since those may interfer with the root widget. You may prefer to have ``App.root = tkinter.Tk()`` and create widgets with ``self.root`` as parent. """ def __init__(self, client, *args, **kwargs): super().__init__(*args, **kwargs) self.cl = client self.me = None self.title(TITLE) self.geometry(SIZE) # Signing in row; the entry supports phone and bot token self.sign_in_label = tkinter.Label(self, text='Loading...') self.sign_in_label.grid(row=0, column=0) self.sign_in_entry = tkinter.Entry(self) self.sign_in_entry.grid(row=0, column=1, sticky=tkinter.EW) self.sign_in_entry.bind('', self.sign_in) self.sign_in_button = tkinter.Button(self, text='...', command=self.sign_in) self.sign_in_button.grid(row=0, column=2) self.code = None # The chat where to send and show messages from tkinter.Label(self, text='Target chat:').grid(row=1, column=0) self.chat = tkinter.Entry(self) self.chat.grid(row=1, column=1, columnspan=2, sticky=tkinter.EW) self.columnconfigure(1, weight=1) self.chat.bind('', self.check_chat) self.chat.bind('', self.check_chat) self.chat.focus() self.chat_id = None # Message log (incoming and outgoing); we configure it as readonly self.log = tkinter.scrolledtext.ScrolledText(self) allow_copy(self.log) self.log.grid(row=2, column=0, columnspan=3, sticky=tkinter.NSEW) self.rowconfigure(2, weight=1) self.cl.add_event_handler(self.on_message, events.NewMessage) # Save shown message IDs to support replying with ".rN reply" # For instance to reply to the last message ".r1 this is a reply" # Deletion also works with ".dN". self.message_ids = [] # Save the sent texts to allow editing with ".s text/replacement" # For instance to edit the last "hello" with "bye" ".s hello/bye" self.sent_text = collections.deque(maxlen=10) # Sending messages tkinter.Label(self, text='Message:').grid(row=3, column=0) self.message = tkinter.Entry(self) self.message.grid(row=3, column=1, sticky=tkinter.EW) self.message.bind('', self.send_message) tkinter.Button(self, text='Send', command=self.send_message).grid(row=3, column=2) # Post-init (async, connect client) self.cl.loop.create_task(self.post_init()) async def post_init(self): """ Completes the initialization of our application. Since `__init__` cannot be `async` we use this. """ if await self.cl.is_user_authorized(): self.set_signed_in(await self.cl.get_me()) else: # User is not logged in, configure the button to ask them to login self.sign_in_button.configure(text='Sign in') self.sign_in_label.configure( text='Sign in (phone/token):') async def on_message(self, event): """ Event handler that will add new messages to the message log. """ # We want to show only messages sent to this chat if event.chat_id != self.chat_id: return # Save the message ID so we know which to reply to self.message_ids.append(event.id) # Decide a prefix (">> " for our messages, "" otherwise) if event.out: text = '>> ' else: sender = await event.get_sender() text = '<{}> '.format(sanitize_str( utils.get_display_name(sender))) # If the message has media show "(MediaType) " if event.media: text += '({}) '.format(event.media.__class__.__name__) text += sanitize_str(event.text) text += '\n' # Append the text to the end with a newline, and scroll to the end self.log.insert(tkinter.END, text) self.log.yview(tkinter.END) # noinspection PyUnusedLocal @callback async def sign_in(self, event=None): """ Note the `event` argument. This is required since this callback may be called from a ``widget.bind`` (such as ``''``), which sends information about the event we don't care about. This callback logs out if authorized, signs in if a code was sent or a bot token is input, or sends the code otherwise. """ self.sign_in_label.configure(text='Working...') self.sign_in_entry.configure(state=tkinter.DISABLED) if await self.cl.is_user_authorized(): await self.cl.log_out() self.destroy() return value = self.sign_in_entry.get().strip() if self.code: self.set_signed_in(await self.cl.sign_in(code=value)) elif ':' in value: self.set_signed_in(await self.cl.sign_in(bot_token=value)) else: self.code = await self.cl.send_code_request(value) self.sign_in_label.configure(text='Code:') self.sign_in_entry.configure(state=tkinter.NORMAL) self.sign_in_entry.delete(0, tkinter.END) self.sign_in_entry.focus() return def set_signed_in(self, me): """ Configures the application as "signed in" (displays user's name and disables the entry to input phone/bot token/code). """ self.me = me self.sign_in_label.configure(text='Signed in') self.sign_in_entry.configure(state=tkinter.NORMAL) self.sign_in_entry.delete(0, tkinter.END) self.sign_in_entry.insert(tkinter.INSERT, utils.get_display_name(me)) self.sign_in_entry.configure(state=tkinter.DISABLED) self.sign_in_button.configure(text='Log out') self.chat.focus() # noinspection PyUnusedLocal @callback async def send_message(self, event=None): """ Sends a message. Does nothing if the client is not connected. """ if not self.cl.is_connected(): return # The user needs to configure a chat where the message should be sent. # # If the chat ID does not exist, it was not valid and the user must # configure one; hint them by changing the background to red. if not self.chat_id: self.chat.configure(bg='red') self.chat.focus() return # Get the message, clear the text field and focus it again text = self.message.get().strip() self.message.delete(0, tkinter.END) self.message.focus() if not text: return # NOTE: This part is optional but supports editing messages # You can remove it if you find it too complicated. # # Check if the edit matches any text m = EDIT.match(text) if m: find = re.compile(m.group(1).lstrip()) # Cannot reversed(enumerate(...)), use index for i in reversed(range(len(self.sent_text))): msg_id, msg_text = self.sent_text[i] if find.search(msg_text): # Found text to replace, so replace it and edit new = find.sub(m.group(2), msg_text) self.sent_text[i] = (msg_id, new) await self.cl.edit_message(self.chat_id, msg_id, new) # Notify that a replacement was made self.log.insert(tkinter.END, '(message edited: {} -> {})\n' .format(msg_text, new)) self.log.yview(tkinter.END) return # Check if we want to delete the message m = DELETE.match(text) if m: try: delete = self.message_ids.pop(-int(m.group(1))) except IndexError: pass else: await self.cl.delete_messages(self.chat_id, delete) # Notify that a message was deleted self.log.insert(tkinter.END, '(message deleted)\n') self.log.yview(tkinter.END) return # Check if we want to reply to some message reply_to = None m = REPLY.match(text) if m: text = m.group(2) try: reply_to = self.message_ids[-int(m.group(1))] except IndexError: pass # NOTE: This part is no longer optional. It sends the message. # Send the message text and get back the sent message object message = await self.cl.send_message(self.chat_id, text, reply_to=reply_to) # Save the sent message ID and text to allow edits self.sent_text.append((message.id, text)) # Process the sent message as if it were an event await self.on_message(message) # noinspection PyUnusedLocal @callback async def check_chat(self, event=None): """ Checks the input chat where to send and listen messages from. """ if self.me is None: return # Not logged in yet chat = self.chat.get().strip() try: chat = int(chat) except ValueError: pass try: old = self.chat_id # Valid chat ID, set it and configure the colour back to white self.chat_id = await self.cl.get_peer_id(chat) self.chat.configure(bg='white') # If the chat ID changed, clear the # messages that we could edit or reply if self.chat_id != old: self.message_ids.clear() self.sent_text.clear() self.log.delete('1.0', tkinter.END) if not self.me.bot: for msg in reversed( await self.cl.get_messages(self.chat_id, 100)): await self.on_message(msg) except ValueError: # Invalid chat ID, let the user know with a yellow background self.chat_id = None self.chat.configure(bg='yellow') async def main(interval=0.05): client = TelegramClient(SESSION, API_ID, API_HASH) try: await client.connect() except Exception as e: print('Failed to connect', e, file=sys.stderr) return app = App(client) try: while True: # We want to update the application but get back # to asyncio's event loop. For this we sleep a # short time so the event loop can run. # # https://www.reddit.com/r/Python/comments/33ecpl app.update() await asyncio.sleep(interval) except KeyboardInterrupt: pass except tkinter.TclError as e: if 'application has been destroyed' not in e.args[0]: raise finally: await app.cl.disconnect() if __name__ == "__main__": asyncio.run(main()) Telethon-1.39.0/telethon_examples/interactive_telegram_client.py000066400000000000000000000403021475566265000252320ustar00rootroot00000000000000import asyncio import os import sys import time from getpass import getpass from telethon import TelegramClient, events from telethon.errors import SessionPasswordNeededError from telethon.network import ConnectionTcpAbridged from telethon.utils import get_display_name def sprint(string, *args, **kwargs): """Safe Print (handle UnicodeEncodeErrors on some terminals)""" try: print(string, *args, **kwargs) except UnicodeEncodeError: string = string.encode('utf-8', errors='ignore')\ .decode('ascii', errors='ignore') print(string, *args, **kwargs) def print_title(title): """Helper function to print titles to the console more nicely""" sprint('\n') sprint('=={}=='.format('=' * len(title))) sprint('= {} ='.format(title)) sprint('=={}=='.format('=' * len(title))) def bytes_to_string(byte_count): """Converts a byte count to a string (in KB, MB...)""" suffix_index = 0 while byte_count >= 1024: byte_count /= 1024 suffix_index += 1 return '{:.2f}{}'.format( byte_count, [' bytes', 'KB', 'MB', 'GB', 'TB'][suffix_index] ) async def async_input(prompt): """ Python's ``input()`` is blocking, which means the event loop we set above can't be running while we're blocking there. This method will let the loop run while we wait for input. """ print(prompt, end='', flush=True) return (await asyncio.get_running_loop().run_in_executor(None, sys.stdin.readline)).rstrip() def get_env(name, message, cast=str): """Helper to get environment variables interactively""" if name in os.environ: return os.environ[name] while True: value = input(message) try: return cast(value) except ValueError as e: print(e, file=sys.stderr) time.sleep(1) class InteractiveTelegramClient(TelegramClient): """Full featured Telegram client, meant to be used on an interactive session to see what Telethon is capable off - This client allows the user to perform some basic interaction with Telegram through Telethon, such as listing dialogs (open chats), talking to people, downloading media, and receiving updates. """ def __init__(self, session_user_id, api_id, api_hash, proxy=None): """ Initializes the InteractiveTelegramClient. :param session_user_id: Name of the *.session file. :param api_id: Telegram's api_id acquired through my.telegram.org. :param api_hash: Telegram's api_hash. :param proxy: Optional proxy tuple/dictionary. """ print_title('Initialization') print('Initializing interactive example...') # The first step is to initialize the TelegramClient, as we are # subclassing it, we need to call super().__init__(). On a more # normal case you would want 'client = TelegramClient(...)' super().__init__( # These parameters should be passed always, session name and API session_user_id, api_id, api_hash, # You can optionally change the connection mode by passing a # type or an instance of it. This changes how the sent packets # look (low-level concept you normally shouldn't worry about). # Default is ConnectionTcpFull, smallest is ConnectionTcpAbridged. connection=ConnectionTcpAbridged, # If you're using a proxy, set it here. proxy=proxy ) # Store {message.id: message} map here so that we can download # media known the message ID, for every message having media. self.found_media = {} async def init(self): # Calling .connect() may raise a connection error False, so you need # to except those before continuing. Otherwise you may want to retry # as done here. print('Connecting to Telegram servers...') try: await self.connect() except IOError: # We handle IOError and not ConnectionError because # PySocks' errors do not subclass ConnectionError # (so this will work with and without proxies). print('Initial connection failed. Retrying...') await self.connect() # If the user hasn't called .sign_in() yet, they won't # be authorized. The first thing you must do is authorize. Calling # .sign_in() should only be done once as the information is saved on # the *.session file so you don't need to enter the code every time. if not await self.is_user_authorized(): print('First run. Sending code request...') user_phone = input('Enter your phone: ') await self.sign_in(user_phone) self_user = None while self_user is None: code = input('Enter the code you just received: ') try: self_user = await self.sign_in(code=code) # Two-step verification may be enabled, and .sign_in will # raise this error. If that's the case ask for the password. # Note that getpass() may not work on PyCharm due to a bug, # if that's the case simply change it for input(). except SessionPasswordNeededError: pw = getpass('Two step verification is enabled. ' 'Please enter your password: ') self_user = await self.sign_in(password=pw) async def run(self): """Main loop of the TelegramClient, will wait for user action""" # Once everything is ready, we can add an event handler. # # Events are an abstraction over Telegram's "Updates" and # are much easier to use. self.add_event_handler(self.message_handler, events.NewMessage) # Enter a while loop to chat as long as the user wants while True: # Retrieve the top dialogs. You can set the limit to None to # retrieve all of them if you wish, but beware that may take # a long time if you have hundreds of them. dialog_count = 15 # Entities represent the user, chat or channel # corresponding to the dialog on the same index. dialogs = await self.get_dialogs(limit=dialog_count) i = None while i is None: print_title('Dialogs window') # Display them so the user can choose for i, dialog in enumerate(dialogs, start=1): sprint('{}. {}'.format(i, get_display_name(dialog.entity))) # Let the user decide who they want to talk to print() print('> Who do you want to send messages to?') print('> Available commands:') print(' !q: Quits the dialogs window and exits.') print(' !l: Logs out, terminating this session.') print() i = await async_input('Enter dialog ID or a command: ') if i == '!q': return if i == '!l': # Logging out will cause the user to need to reenter the # code next time they want to use the library, and will # also delete the *.session file off the filesystem. # # This is not the same as simply calling .disconnect(), # which simply shuts down everything gracefully. await self.log_out() return try: i = int(i if i else 0) - 1 # Ensure it is inside the bounds, otherwise retry if not 0 <= i < dialog_count: i = None except ValueError: i = None # Retrieve the selected user (or chat, or channel) entity = dialogs[i].entity # Show some information print_title('Chat with "{}"'.format(get_display_name(entity))) print('Available commands:') print(' !q: Quits the current chat.') print(' !Q: Quits the current chat and exits.') print(' !h: prints the latest messages (message History).') print(' !up : Uploads and sends the Photo from path.') print(' !uf : Uploads and sends the File from path.') print(' !d : Deletes a message by its id') print(' !dm : Downloads the given message Media (if any).') print(' !dp: Downloads the current dialog Profile picture.') print(' !i: Prints information about this chat..') print() # And start a while loop to chat while True: msg = await async_input('Enter a message: ') # Quit if msg == '!q': break elif msg == '!Q': return # History elif msg == '!h': # First retrieve the messages and some information messages = await self.get_messages(entity, limit=10) # Iterate over all (in reverse order so the latest appear # the last in the console) and print them with format: # "[hh:mm] Sender: Message" for msg in reversed(messages): # Note how we access .sender here. Since we made an # API call using the self client, it will always have # information about the sender. This is different to # events, where Telegram may not always send the user. name = get_display_name(msg.sender) # Format the message content if getattr(msg, 'media', None): self.found_media[msg.id] = msg content = '<{}> {}'.format( type(msg.media).__name__, msg.message) elif hasattr(msg, 'message'): content = msg.message elif hasattr(msg, 'action'): content = str(msg.action) else: # Unknown message, simply print its class name content = type(msg).__name__ # And print it to the user sprint('[{}:{}] (ID={}) {}: {}'.format( msg.date.hour, msg.date.minute, msg.id, name, content)) # Send photo elif msg.startswith('!up '): # Slice the message to get the path path = msg[len('!up '):] await self.send_photo(path=path, entity=entity) # Send file (document) elif msg.startswith('!uf '): # Slice the message to get the path path = msg[len('!uf '):] await self.send_document(path=path, entity=entity) # Delete messages elif msg.startswith('!d '): # Slice the message to get message ID msg = msg[len('!d '):] deleted_msg = await self.delete_messages(entity, msg) print('Deleted {}'.format(deleted_msg)) # Download media elif msg.startswith('!dm '): # Slice the message to get message ID await self.download_media_by_id(msg[len('!dm '):]) # Download profile photo elif msg == '!dp': print('Downloading profile picture to usermedia/...') os.makedirs('usermedia', exist_ok=True) output = await self.download_profile_photo(entity, 'usermedia') if output: print('Profile picture downloaded to', output) else: print('No profile picture found for this user!') elif msg == '!i': attributes = list(entity.to_dict().items()) pad = max(len(x) for x, _ in attributes) for name, val in attributes: print("{:<{width}} : {}".format(name, val, width=pad)) # Send chat message (if any) elif msg: await self.send_message(entity, msg, link_preview=False) async def send_photo(self, path, entity): """Sends the file located at path to the desired entity as a photo""" await self.send_file( entity, path, progress_callback=self.upload_progress_callback ) print('Photo sent!') async def send_document(self, path, entity): """Sends the file located at path to the desired entity as a document""" await self.send_file( entity, path, force_document=True, progress_callback=self.upload_progress_callback ) print('Document sent!') async def download_media_by_id(self, media_id): """Given a message ID, finds the media this message contained and downloads it. """ try: msg = self.found_media[int(media_id)] except (ValueError, KeyError): # ValueError when parsing, KeyError when accessing dictionary print('Invalid media ID given or message not found!') return print('Downloading media to usermedia/...') os.makedirs('usermedia', exist_ok=True) output = await self.download_media( msg.media, file='usermedia/', progress_callback=self.download_progress_callback ) print('Media downloaded to {}!'.format(output)) @staticmethod def download_progress_callback(downloaded_bytes, total_bytes): InteractiveTelegramClient.print_progress( 'Downloaded', downloaded_bytes, total_bytes ) @staticmethod def upload_progress_callback(uploaded_bytes, total_bytes): InteractiveTelegramClient.print_progress( 'Uploaded', uploaded_bytes, total_bytes ) @staticmethod def print_progress(progress_type, downloaded_bytes, total_bytes): print('{} {} out of {} ({:.2%})'.format( progress_type, bytes_to_string(downloaded_bytes), bytes_to_string(total_bytes), downloaded_bytes / total_bytes) ) async def message_handler(self, event): """Callback method for received events.NewMessage""" # Note that message_handler is called when a Telegram update occurs # and an event is created. Telegram may not always send information # about the ``.sender`` or the ``.chat``, so if you *really* want it # you should use ``get_chat()`` and ``get_sender()`` while working # with events. Since they are methods, you know they may make an API # call, which can be expensive. chat = await event.get_chat() if event.is_group: if event.out: sprint('>> sent "{}" to chat {}'.format( event.text, get_display_name(chat) )) else: sprint('<< {} @ {} sent "{}"'.format( get_display_name(await event.get_sender()), get_display_name(chat), event.text )) else: if event.out: sprint('>> "{}" to user {}'.format( event.text, get_display_name(chat) )) else: sprint('<< {} sent "{}"'.format( get_display_name(chat), event.text )) async def main(): SESSION = os.environ.get('TG_SESSION', 'interactive') API_ID = get_env('TG_API_ID', 'Enter your API ID: ', int) API_HASH = get_env('TG_API_HASH', 'Enter your API hash: ') client = InteractiveTelegramClient(SESSION, API_ID, API_HASH) await client.init() await client.run() if __name__ == '__main__': asyncio.run() Telethon-1.39.0/telethon_examples/payment.py000066400000000000000000000143771475566265000211710ustar00rootroot00000000000000from telethon import TelegramClient, events, types, functions import asyncio import logging import tracemalloc import os import time import sys """ Provider token can be obtained via @BotFather. more info at https://core.telegram.org/bots/payments#getting-a-token If you are using test token, set test=True in generate_invoice function, If you are using real token, set test=False """ provider_token = '' tracemalloc.start() logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.WARNING) logger = logging.getLogger(__name__) def get_env(name, message, cast=str): if name in os.environ: return os.environ[name] while True: value = input(message) try: return cast(value) except ValueError as e: print(e, file=sys.stderr) time.sleep(1) bot = TelegramClient( os.environ.get('TG_SESSION', 'payment'), get_env('TG_API_ID', 'Enter your API ID: ', int), get_env('TG_API_HASH', 'Enter your API hash: '), proxy=None ) # That event is handled when customer enters his card/etc, on final pre-checkout # If we don't `SetBotPrecheckoutResultsRequest`, money won't be charged from buyer, and nothing will happen next. @bot.on(events.Raw(types.UpdateBotPrecheckoutQuery)) async def payment_pre_checkout_handler(event: types.UpdateBotPrecheckoutQuery): if event.payload.decode('UTF-8') == 'product A': # so we have to confirm payment await bot( functions.messages.SetBotPrecheckoutResultsRequest( query_id=event.query_id, success=True, error=None ) ) elif event.payload.decode('UTF-8') == 'product B': # same for another await bot( functions.messages.SetBotPrecheckoutResultsRequest( query_id=event.query_id, success=True, error=None ) ) else: # for example, something went wrong (whatever reason). We can tell customer about that: await bot( functions.messages.SetBotPrecheckoutResultsRequest( query_id=event.query_id, success=False, error='Something went wrong' ) ) raise events.StopPropagation # That event is handled at the end, when customer payed. @bot.on(events.Raw(types.UpdateNewMessage)) async def payment_received_handler(event): if isinstance(event.message.action, types.MessageActionPaymentSentMe): payment: types.MessageActionPaymentSentMe = event.message.action # do something after payment was received if payment.payload.decode('UTF-8') == 'product A': await bot.send_message(event.message.peer_id.user_id, 'Thank you for buying product A!') elif payment.payload.decode('UTF-8') == 'product B': await bot.send_message(event.message.peer_id.user_id, 'Thank you for buying product B!') raise events.StopPropagation # let's put it in one function for more easier way def generate_invoice(price_label: str, price_amount: int, currency: str, title: str, description: str, payload: str, start_param: str) -> types.InputMediaInvoice: price = types.LabeledPrice(label=price_label, amount=price_amount) # label - just a text, amount=10000 means 100.00 invoice = types.Invoice( currency=currency, # currency like USD prices=[price], # there could be a couple of prices. test=True, # if you're working with test token, else set test=False. # More info at https://core.telegram.org/bots/payments # params for requesting specific fields name_requested=False, phone_requested=False, email_requested=False, shipping_address_requested=False, # if price changes depending on shipping flexible=False, # send data to provider phone_to_provider=False, email_to_provider=False ) return types.InputMediaInvoice( title=title, description=description, invoice=invoice, payload=payload.encode('UTF-8'), # payload, which will be sent to next 2 handlers provider=provider_token, provider_data=types.DataJSON('{}'), # data about the invoice, which will be shared with the payment provider. A detailed description of # required fields should be provided by the payment provider. start_param=start_param, # Unique deep-linking parameter. May also be used in UpdateBotPrecheckoutQuery # see: https://core.telegram.org/bots#deep-linking # it may be the empty string if not needed ) @bot.on(events.NewMessage(pattern='/start')) async def start_handler(event: events.NewMessage.Event): await event.respond('/product_a - product A\n/product_b - product B\n/product_c - product, shall cause an error') @bot.on(events.NewMessage(pattern='/product_a')) async def start_handler(event: events.NewMessage.Event): await bot.send_message( event.chat_id, 'Sending invoice A', file=generate_invoice( price_label='Pay', price_amount=10000, currency='RUB', title='Title A', description='description A', payload='product A', start_param='abc' ) ) @bot.on(events.NewMessage(pattern='/product_b')) async def start_handler(event: events.NewMessage.Event): await bot.send_message( event.chat_id, 'Sending invoice B', file=generate_invoice( price_label='Pay', price_amount=20000, currency='RUB', title='Title B', description='description B', payload='product B', start_param='abc' ) ) @bot.on(events.NewMessage(pattern='/product_c')) async def start_handler(event: events.NewMessage.Event): await bot.send_message( event.chat_id, 'Sending invoice C', file=generate_invoice( price_label='Pay', price_amount=50000, currency='RUB', title='Title C', description='description c - shall cause an error', payload='product C', start_param='abc' ) ) async def main(): await bot.start() await bot.run_until_disconnected() if __name__ == '__main__': if not provider_token: logger.error("No provider token supplied.") exit(1) asyncio.run(main()) Telethon-1.39.0/telethon_examples/print_messages.py000066400000000000000000000027411475566265000225270ustar00rootroot00000000000000#!/usr/bin/env python3 # A simple script to print some messages. import os import sys import time from telethon import TelegramClient, events, utils def get_env(name, message, cast=str): if name in os.environ: return os.environ[name] while True: value = input(message) try: return cast(value) except ValueError as e: print(e, file=sys.stderr) time.sleep(1) session = os.environ.get('TG_SESSION', 'printer') api_id = get_env('TG_API_ID', 'Enter your API ID: ', int) api_hash = get_env('TG_API_HASH', 'Enter your API hash: ') proxy = None # https://github.com/Anorov/PySocks # Create and start the client so we can make requests (we don't here) client = TelegramClient(session, api_id, api_hash, proxy=proxy).start() # `pattern` is a regex, see https://docs.python.org/3/library/re.html # Use https://regexone.com/ if you want a more interactive way of learning. # # "(?i)" makes it case-insensitive, and | separates "options". @client.on(events.NewMessage(pattern=r'(?i).*\b(hello|hi)\b')) async def handler(event): sender = await event.get_sender() name = utils.get_display_name(sender) print(name, 'said', event.text, '!') try: print('(Press Ctrl+C to stop this)') client.run_until_disconnected() finally: client.disconnect() # Note: We used try/finally to show it can be done this way, but using: # # with client: # client.run_until_disconnected() # # is almost always a better idea. Telethon-1.39.0/telethon_examples/print_updates.py000077500000000000000000000026051475566265000223670ustar00rootroot00000000000000#!/usr/bin/env python3 # A simple script to print all updates received. # Import modules to access environment, sleep, write to stderr import os import sys import time # Import the client from telethon import TelegramClient # This is a helper method to access environment variables or # prompt the user to type them in the terminal if missing. def get_env(name, message, cast=str): if name in os.environ: return os.environ[name] while True: value = input(message) try: return cast(value) except ValueError as e: print(e, file=sys.stderr) time.sleep(1) # Define some variables so the code reads easier session = os.environ.get('TG_SESSION', 'printer') api_id = get_env('TG_API_ID', 'Enter your API ID: ', int) api_hash = get_env('TG_API_HASH', 'Enter your API hash: ') proxy = None # https://github.com/Anorov/PySocks # This is our update handler. It is called when a new update arrives. async def handler(update): print(update) # Use the client in a `with` block. It calls `start/disconnect` automatically. with TelegramClient(session, api_id, api_hash, proxy=proxy) as client: # Register the update handler so that it gets called client.add_event_handler(handler) # Run the client until Ctrl+C is pressed, or the client disconnects print('(Press Ctrl+C to stop this)') client.run_until_disconnected() Telethon-1.39.0/telethon_examples/quart_login.py000066400000000000000000000103461475566265000220300ustar00rootroot00000000000000import base64 import os from quart import Quart, render_template_string, request from telethon import TelegramClient, utils from telethon.errors import SessionPasswordNeededError def get_env(name, message): if name in os.environ: return os.environ[name] return input(message) BASE_TEMPLATE = ''' Telethon + Quart {{ content | safe }} ''' PHONE_FORM = '''
Phone (international format):
''' CODE_FORM = '''
Telegram code:
''' PASSWORD_FORM = '''
Telegram password:
''' # Session name, API ID and hash to use; loaded from environmental variables SESSION = os.environ.get('TG_SESSION', 'quart') API_ID = int(get_env('TG_API_ID', 'Enter your API ID: ')) API_HASH = get_env('TG_API_HASH', 'Enter your API hash: ') # Telethon client client = TelegramClient(SESSION, API_ID, API_HASH) client.parse_mode = 'html' # <- Render things nicely phone = None # Quart app app = Quart(__name__) app.secret_key = 'CHANGE THIS TO SOMETHING SECRET' # Helper method to format messages nicely async def format_message(message): if message.photo: content = '{}'.format( base64.b64encode(await message.download_media(bytes)).decode(), message.raw_text ) else: # client.parse_mode = 'html', so bold etc. will work! content = (message.text or '(action message)').replace('\n', '
') return '

{}: {}{}

'.format( utils.get_display_name(message.sender), content, message.date ) # Connect the client before we start serving with Quart @app.before_serving async def startup(): # After connecting, the client will create additional asyncio tasks that run until it's disconnected again. # Be careful to not mix different asyncio loops during a client's lifetime, or things won't work properly! await client.connect() # After we're done serving (near shutdown), clean up the client @app.after_serving async def cleanup(): await client.disconnect() @app.route('/', methods=['GET', 'POST']) async def root(): # We want to update the global phone variable to remember it global phone # Check form parameters (phone/code) form = await request.form if 'phone' in form: phone = form['phone'] await client.send_code_request(phone) if 'code' in form: try: await client.sign_in(code=form['code']) except SessionPasswordNeededError: return await render_template_string(BASE_TEMPLATE, content=PASSWORD_FORM) if 'password' in form: await client.sign_in(password=form['password']) # If we're logged in, show them some messages from their first dialog if await client.is_user_authorized(): # They are logged in, show them some messages from their first dialog dialog = (await client.get_dialogs())[0] result = '

{}

'.format(dialog.title) async for m in client.iter_messages(dialog, 10): result += await(format_message(m)) return await render_template_string(BASE_TEMPLATE, content=result) # Ask for the phone if we don't know it yet if phone is None: return await render_template_string(BASE_TEMPLATE, content=PHONE_FORM) # We have the phone, but we're not logged in, so ask for the code return await render_template_string(BASE_TEMPLATE, content=CODE_FORM) # By default, `Quart.run` uses `asyncio.run()`, which creates a new asyncio # event loop. If we had connected the `TelegramClient` before, `telethon` will # use `asyncio.get_running_loop()` to create some additional tasks. If these # loops are different, it won't work. # # To keep things simple, be sure to not create multiple asyncio loops! if __name__ == '__main__': app.run() Telethon-1.39.0/telethon_examples/replier.py000077500000000000000000000060631475566265000211520ustar00rootroot00000000000000#!/usr/bin/env python3 """ A example script to automatically send messages based on certain triggers. 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. """ import os import sys import time from collections import defaultdict from telethon import TelegramClient, events import logging logging.basicConfig(level=logging.WARNING) # "When did we last react?" dictionary, 0.0 by default recent_reacts = defaultdict(float) def get_env(name, message, cast=str): if name in os.environ: return os.environ[name] while True: value = input(message) try: return cast(value) except ValueError as e: print(e, file=sys.stderr) time.sleep(1) def can_react(chat_id): # Get the time when we last sent a reaction (or 0) last = recent_reacts[chat_id] # Get the current time now = time.time() # If 10 minutes as seconds have passed, we can react if now - last < 10 * 60: # Make sure we updated the last reaction time recent_reacts[chat_id] = now return True else: return False # Register `events.NewMessage` before defining the client. # Once you have a client, `add_event_handler` will use this event. @events.register(events.NewMessage) async def handler(event): # There are better ways to do this, but this is simple. # If the message is not outgoing (i.e. someone else sent it) if not event.out: if 'emacs' in event.raw_text: if can_react(event.chat_id): await event.reply('> emacs\nneeds more vim') elif 'vim' in event.raw_text: if can_react(event.chat_id): await event.reply('> vim\nneeds more emacs') elif 'chrome' in event.raw_text: if can_react(event.chat_id): await event.reply('> chrome\nneeds more firefox') # Reply always responds as a reply. We can respond without replying too if 'shrug' in event.raw_text: if can_react(event.chat_id): await event.respond(r'¯\_(ツ)_/¯') # We can also use client methods from here client = event.client # If we sent the message, we are replying to someone, # and we said "save pic" in the message if event.out and event.is_reply and 'save pic' in event.raw_text: reply_msg = await event.get_reply_message() replied_to_user = await reply_msg.get_input_sender() message = await event.reply('Downloading your profile photo...') file = await client.download_profile_photo(replied_to_user) await message.edit('I saved your photo in {}'.format(file)) client = TelegramClient( os.environ.get('TG_SESSION', 'replier'), get_env('TG_API_ID', 'Enter your API ID: ', int), get_env('TG_API_HASH', 'Enter your API hash: '), proxy=None ) with client: # This remembers the events.NewMessage we registered before client.add_event_handler(handler) print('(Press Ctrl+C to stop this)') client.run_until_disconnected() Telethon-1.39.0/telethon_examples/screenshot-gui.jpg000066400000000000000000000626321475566265000226000ustar00rootroot00000000000000JFIFC    $.' ",#(7),01444'9=82<.342C  2!!222222222222222222222222222222222222222222222222225 Z)n@, k0n;lbj5Dj9 WؐPafP-Nk˺3IH4&3 ĂϤ2d@@i18 ^QKbvTsyPt'&ZQ[r1! )LJ\ U ͆aW j'&mϨF6tsCFC,MeatbWAΌ϶EXŘa N5O>ӖG4K>gӢ*&A-f ĵ+$Ʌϓ `L"If)H11, e"D<6Kӏ/̎pZQ%,J3i֜pQL>KCYH]HG-HeY#+O IQn-JSaA/Yu+)r)& Q $bY7μsy>5b^ɉ`m7gHqByѐf1JB$NDd HqYtG#єƲto+MEx<fBԮ"&1fW5)di/Qa^ue)rRFi˟K"ЈD>4IO,27K9Z3VRIb^dy dJ4`sy>58Tdt l,L s21go)r%^ZEeH:x< 417M&|5HĂ)`VE\I0&ĚHAl3OC9r] LG~s~p6_sy?fL"ܣ% iPA,M!Mfq4r2! Fo)$],aX,wU|pSL9٠Y%7INݒH{)%-=#G])tҗJ])tҗJ])tҗJ])tҗJ])tҗJ])tҗJ])tҗJ])tҗJ])tҗJ])tҗJ])tҗJ])tҗJ])tҗJ])tҗJ])tҗJ])tҗJ])tҗJ])tH]"Qt.E(1udI@H(VH6ZaZFm> _Nv;;@:xŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎŎ01F8pRyJ+I^շ+G"̸Ȏ _=d~6!`ΨOǸr*g,Vf֮u?Zyoz'^ZZߢiY5rdeYupQR hHlؕ ?)-[~.'ͣV<0ū?X0k/,^Xyb/,^Xyb/,^Xyb/,^Xyb/,^Xyb/,^Xyb/,^Xyb/,^[JV,PϥrRJf!cʎLV)iKKQ'Ë$=}]tkd0&Ly1GQF`H#p⌣it#G\V X+`V X+`V X+`V X+`V X+`V X+`V X+`X4+UqIjG3_%lnqEhY|a+C9_N)-/G ]ƗNH:dbS$J~^v,{=GL %1Kmm۠BxCdR]2a:LX}ߑռm%ͭqy @\$r@\$r@N&$r@\$r@\$r@\$r@\$r@\$r@\$r@\$r@\$r@\$r@\$r@\$r@\$r@\$r@\gǭ+EqhVA'E 2h (Yw83 DB_b6{$^q} uύ]*`^ K,OSAF@ ⎏;8@',GdO%y+R6SDVL(q;\74 U&K.q# &r AnQ_/F!4+wdᑌ 7(ߠκ\ 'tz+p&U B=fm#7]{z#!l2R]%hd J$G ɬȃݸһ|jg K+ c fG$L9w)1bG;SJPi|[dBkM9Dl G4DJv-ǎِ%K$iJTYwd>UTvHK@2)x}|Xb '˒} [NE.scO~9Ǐ4v6EO]) %++ℭyǼ ,! SmG*Sw m{\ -a26˔5K_"ɐÕ(G›Kuai⟻sTV`f"yF0&]E cx $(Fe|PMrsKK$2t5ݑ>o^SHwXC-2cbP~Yz7 e9${+]fr8Zs[\t%Z&h!wʡlώ+@J jlG=!6i- m"v bٝW~{C+VtY")9rh3Ř}w\D5"ѭ :jPVP|']&Oė6QBJIGc.26d!KC+eP5#/}^!Gh$bv2eΐ!}܌).~r[i:Qd!TA" &lUC==cZ9d}PH&&y/K=LCdo#ݞw1 1v4PM4/{;~oI\qi~GRg+D0L !:>+42h[l r[-fΊ F I;FZBt7Ҷ&ZGlk6ʌmm]lad] H :qo8juӮ0Qm!4q"a`x@TA0 -}D˪dLCH8f+l$N z**TcbJB٢JBg[#D̙|3igfC-t;{E%,Žռ8+GGRg(7:/1c”ҩY{{Ky(gh:D $ʥ孲ȉS>UgYR)s2Xrm%&qF#ɗ 4lKg8>#ߩ^3(hc Mj}U-l T0jT@yXh[!^Si⸁򭲣0Vc«iX2%c~ԯhevFGsr') Xyү37?, r~EɎ%C[P6)dK1KC}!򥜏B<;~<<1*6eSN%.` ܮ>jYkYkWibG(sg z:i@<2Ի :|FӖx9 hȃ:UdiӬ8#ߩ^3(h@0eEaMMaJW@E| D+{SisH=&jq\gAG" kBBwd>3+Dxu#$IcԬ8;.mG)1[vUEmAvxGI <JtNdpF 0`"flanH{69iZyؤ{+]frC[R]FyHʹ԰%Ì6Cmj).q{"V5bM٧pCqp:Pe8Щw\FzHY(ѓ ,+CFyCbEM6bT_zY0Acʣb<*PTqXQFe )&خL*҇kPO! ʖ+LzD,$?SQucՕ=W2|ԗ8gk4s0 V=C)T:y=!"6$VcH9!D"iYN4 ̤scя91fcm1ҿA.)+0 YhyF[#ߩ^308hODEa('> X%4sH/lc8 Ip8ŤxqzK͇%["%5,S8/,:q]ap6W̠GaEZV-|Л%.th|g}GRg/OPċ*8ƛ6-ZMS{RYB6.Tr}#ߩ^30% ;9sĈw#] *!e2 HlX{U͏4yyLh%tm;I$k%H_ z "QLL> C#YoK˼٭H=!fۜ D84ŊFh"P{ģDqd=YH?{+]frČbAjٴlcGRg/OPHW2|JWo?C#ߩ^3(ä{+]frt~{C+^ԯhev?= 39z:o$Ag+(+Xue e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e e NcJFX mn'+obQ0ƒi+Jƒi+Jƒi+Jƒi+Jƒi+Jƒi+Jƒi+Jƒi+Jƒi+Jƒi+Jƒi+Jƒi+Jƒi+Jƒi+Jƒi*;?XZ\~RZ\E|يؼ3o%?A)GܤluxOMT&.iDҘs{yi1mA6-fsoR,E`ځ~\36{dK^äZsڊEE4Qnr4tcIJ̪׵#pYQǯB]/e Q_m-Xmcd۬[gknl[n'(= ziqe姺Aqc-erP.zk%Y#+#E\˞=,z3GMozXm=]~?5cXcV?Տ~j?5cXcV?Տ~j?5cXcV?Տ~j?5cXcV?Տ~j?5cXcV?Տ~j?5cXcV?Տ~j?5cXcV?Տ~j?5cXcV?Տ~j?5cXcV??=IcԞ=IcԞ=IcԞ=IڂcRf/=͙\@~K/$5;fW K-)%A?ZeF_ZeF_ZeF_ZeF_ZeF_ZeF_ZeF_ZeF_ZeF_ZeF_ZeF_ZeF_ZeF_ZeF_ZeF_ZeF_ZeF_ZeF_ZeF_ZeF_ZeF_ZeF_ZeF_ZeF_ZeF_ZeF_ZeF_ZeF_ZeF_Z^tƺ鲥͇&ȊYP׈}ﯹOux\r`/mc:)b|u+H$g*Kkz=IKѯH?C̡ ɳ̅7#Vaogm/-mK.fЯ9mg]#)cPO&%M$rXI_yvQldnfUh MaeLG)H9[oF5+` .2~jx#kW}ﯹOwѸA=3r}'0*{ҷ+qR+qR+qR+qR+qR+qR+qR+qR+qR+qR+qR+qR+qR+qR+qR+qR+qR+qR+qR+qR+qR+qR+qR+qR+qR+qR+qR+qR+qR)SFcaS*xOĜ԰(a<\bTђPuW?pB9^@ԩ*qQ{\} \ o,l;Z( #q}Ʊo"0 Uv6u>p3{ѵ H#{y}$7:?*ջ}btap˔T=Պp u-O/ST=8:qt)ӌSN1Nb8Iˉ,ZkNb8:qt)ӌSN1Nb8:qt)ӌSN1Nb8:qt)ӌSN1Nb8:qt)ӌSN1Nb8:qt)ӌSN1Nb8:qt)ӌSN1Nb8:qt)ӌSN1Nb'XbNfmy-=9ZT Am9Bbr]Ge7C[q:` smrwiK,4HNѿɾb/ԤISm-z+B[{&̛D6:'魥%+.MX˧CLnjc*ba$q; Z*_Ŷ]=7:lk_KcIu/NVٵamo.M kfhmRd#1%y_J`2ok~2䁧PTpD(R-}ָBIr6B7-uѕvʻmkTeaVl`.:Bfە7ۉS;3!M`x-kRd>Fa,zH4&bJmT9s)lmyME6|'uZ]țk$@X'Zװ}#C!9Frt+uXXIjjm$ #߸=<3ؽ'S"Smn[~5)ϝ0x}).î!b22kzo!B= jLNR~AtkPp$F3k~F1a52 ;,H,^Y$Q铑l\{FLC)[X4$\ڼ Gڒfч/`!6Z1Wؕd-kM 7hqO/^G|Msۮ948_71je³4-{ذ!DƎAbGPGU$9:_FHaJkqҖ;剐_v*h!rKa{ipmzx:Oy,T.Sg7w9kٗ)Bd[YXJ {hK)[AݼQx:QKziUK}|bIlIUkmwuRa!hSAl:))n#"C$~[Cыa>%Z9͘嵺߮wI9=LĊ6Υo[1>\2wi#Iu)I ~yF3BKJPפR"غaqޙ9q x>& iRđ<ë[U.Tae+]FEY-#Jή54fga;s`9@ߦ.X?-sZY<3rԕoXh^G@\/L[f}icC3jw*:Ss ĜMZbOjeYIȻ2DLC0& lk)Mu:3I7L bU$ker:k}ġ;C[QA/&g%lnRFz(!$yE"]S~G4ڝ:Pr!8|*#[)uPOu窕[)߭aM2&Vq3_M#d~CecIb'vEgkP([r;.|;-Z>PN";bܻ̒oc-bm< E~nql&ydةϰØ0k3i_+Ix#ٵ܋asjͽNš$=1XeMxQ}FQH$FMЯMLTn>b. xFˠ:y4^kraf_[\ot 3#,&]ucS1AXtLۋiV i(]($G;^9߹oz׈}ğLzO6PS _), JX TWL\$#nzO>6ABSm-m^8ٵkZ~$]fKXծE7nI3_[r $KR}mS@kF+ "mEwM"ߏ'ARWd3gV,W a2}j١ۚN^7%ss?0ɰwȩ7HgPm$-|ѬGb+4֏U|n}[Pl_TQC]sz75D"im'u;U?ӭToDf )p[|yﺰf}pX!խKN#67䯌|k Jϝ,Iz #)qmBdX@yħmքR b9d龖M;c,2y؁^g1~[TmcH,ru CkGti[ &) i*_]bqp˱խt$b!"rX 9v4#YJ7U8ͻAz<̐eA ]z?:x3nu>0'viT7/xmkxQ 志ڑS2DcWk ]]ʖQC%Ma@8  cLylԝj,𫛔+7g3~5"dw.OU`|yk a[8 =,iy6 +]N_=4m+e ЀHKymtΧm!@.9Ub0͍GSӠuH+2+ƶzxՎGRbċtBb2͐̾c^ʬbNns33Hkff7&ՔR 2ePƻ1,eCy8^eE#YS7(P\(S Sp~ .X+w?oz׈}ğK3ߝmo؛&.6Z yc<IIr[4/ 6 -\#iѱL`.M@ 2sֶ\=zޱsJ鉓/ɴ]Y7<{U?8 J0#K%vC7UgVr"±m&Â_A 5JBO*)Sx;>%\Gs[5q0s4z6V- y s|ؖ`G V! 0.,FOI:*t&8$ϼ 7Ǎ˶e$j. [cr3}鲟}49;FZk 붥\ʱo9|lC%jmQm,,AbQbWF} L3h NڟdP/ }Lq-297N)ʤt^$3&s&) ĂkNsai -,4o7U^xG<퉉\[zD _ i ,,?LbɝQ5;eŅc2Y(R( [޵u1'5_kKwwi svyݻ'3@Wkv-997I[w|#mn7fCگŽJf\DWǓmM=X0SeEk]/osd]P{\<^V;Ȋmd^{ >^!W~Xs,&d0֘ϋ/i:zEMTrPLϊ,r92.Knk cF|]o{^zNqEaeŮ̩!7yk_&=f}Z2Ge\l:Xi$0wW+E*㛔WۘGcc{Vx<6d2gF2??`1l~C?Eg#G_c-06ASY#8wIe0+ V{X>HWmXFp\#l.mnT\L5n>䏞Q$+*2!P\+mlyӮ0d3u E䏞x_INet!ta+ x'G!5qk-qt_u&g|Vծ9J;ɩ#i* YnmՀ VVBRjw1]ȞEiFD 0k w -y3kB-Q<"VbG+*dd]NU{P 91l{e6$;3P<JnFI+;򵮦sm1:s} ɇt Ju˧M I)x3K)f~LHFڴb?{V$e0IZXMe#vS}N:+M[~|$orsYr0%E:mXC.emK{뾠ь𝂑3euHHVǓJDOQ[/}G1j&O q-m,VL&Vc d_MGlΪsiފ4 2f!ᾰ"B@3:'l3_c~\1&-l6Ώm5]::jbOkOп8xD; %|"Dٛ*1?@ѯoz׈}ğ׊r?Q?B^!W~^*~G}{ּC$FT x_Ix-ZS/;[޵u1'5_wܷk>bOkOп:6;֎!v_ʿD#jz^Wz^Wz^Wz^Wz^Wz^Wz^Wz^Wz^Wz^B)ߧU"ÖMF=K.纖.3KR{b=ԱqXu,\g.3KR{b=ԱqXu,\g.3KR{b=ԱqXu,\g.3KR{b=ԱqXu,\g.3KR{b=ԱqXu,\g.3KR{b=ԱqXu,\g.3KR{b=ԱqXu,\g.3KRǗjoƝvioƝv}|T8SkX.(p .HodɭTFʭ@ tREm'g!H){9bΈʐ[Fڶ q&9JL]H͟nN_wκ3"Ilt ѭOiGnV-8HXsr؅$sP%6˂@@׬ѻnIMx}SKsg]wޣ5-}k@8}QrP-Ŭm{t (IvYX?_=+@זB؄E5555555555555555555555555555555555555555555555555555555555556ࣕ#*.Flsf;Z Yfr[_y Bw˕}zLUw"O5`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_`{_E/_*)}bQKʊ_XTR/ :_+*;g*!1QaAqP` @0? 8gY.0נ0pʑ&?|X6ڑ5LJ0ڐ5z[*SBSpdlFI xF(3(5-mP8A{ 6J~X]I-S|f{DҶ:^'O-upG q\'oFxS3 DO- Z?8Kةɞη"zo>pۻsX%5c R :OUa)2 {44h?@F].tKZ}J􊄤o],}7)-ibgj/=>|τ>j冚L8i.T+!Lx7`+ ic?)tǞ5X 4R:N/N}X*rхC˗.\r˗.\r˗.\r˗.\r˗.\r˗.\r˗.\r@Z sGauX*n(%מ Xۛh.'Stm@.pTeۙx [a{a%xQD^8IOx@wZS{\&s7OLHaV6tW 9?=׵F&;m J"5\LPcݸy՞K<7O+E@9;x EVw% d}#>HϤ|g>3G}#>HϤ|g>3G}#>HϤ|g>3G}#>HϤ|g>3G}#>HϤ|g>3G}#>HϤ|g>3G}#>HϤ|g>3G}#>H5D}ǧl(ܓryɀ J\0i2s^cB}Uw}#P X, ~l'1Z9 9BCPFQp.%2sg4v(ۢVa""JdPn\͜͜͜͜͜͜͜͜͜͜͜͜͜͜͜͜͜͜͜͜͜͜͜͜͜͜͜͜%xu)rw8nvrSXQT8HBܺ 1ރI 'nn0F 0ՓơfHvnp^5XUBWð ,&CL=쬛Vx]o:l2A{BQQfvl9bPQ`ƫl[Y܉O%׃X"el>MaWճl}[>vPPAAPFZ              }]n2z*u|o l4$;2~6{)+SO3Kq/wkP0 ɠ4`{& hTbټ/ DS@K#[xbLѿ勉:BҨjCy҈hgb:& (+쮛dG-2Mc>CA`̴A`)֥!ǻ &vũF(Aob8EZK 4.I]{ȡ&j:Ix p  {>|nb*^ʯiT[ hPa?,:BpI$Z䦘4{ ݸ^ʯi6%Pkxx<(u+E ?T՗]+ۻ5cj%v\90`1 !i'iH@, Z_5o̜:m<bC#杳>XcOh'0kh :NGZƴC+_޵_atDꨒ&Q(U9P\\s{2v[cDQoTGc235{ ֵ@*6Pw! E0D5^g87Cu `|;` RR:%YM @"0V\4C+H(K$p"]w0y4S^9HzI!Oe-D @0@r0`/I!)/F@1 dS):D Wag;>^Sd`f=M~2zbfT&^E81LpmC$E"!9}z ,&6`TɆ6 a]y!4PoȲ+*Wڨh`jf$^ N*}~6`4E=}=l% k*A: eiQUN@;sGCa;@ tDlpmwy76=55.䓾~?Ӏ/Jmbak']QIP0N< u0s[J#NF7g0 yzJH'#-Q'H21<Tp# H09( fF]ƒ2hD^{"A=cw2k\NN++ΎdnW&'881BPȦWYR>FSpj(0e )A Zl`uJDv fH7ǡP[@NtW-–Н+!t8C'aTD!v;WYY-#'C`i@`ܚwV=% l) b1R8$7a M,17jv(rWK2AdpnOVnk)a3m xp{DXڡҚ[לtVbdG,9X0ђzxFĞQk;&hrjxP(=UU& <"i= UW0 (LWfs%zm0F( gwh @%DLnr yt6{oR=Ƥ ONߒ9ۘفPggi’$x $1 !ܑh3jpZl -0`b 5-Y>ċ.@#pN .!(؃p̢E '>S8Y 1k$=d럠!b4zkDPn]'Då{ rhخƣt]Ѕ@ɝC`}Q6v~@FUI8@)-Nj,5+́-hnZU?Pߋ .W^*#}=:R(ezma:IeOҹyz T1w+GeI `BW gـ 9>×'@q (Ѩcs)g-.BCu@ ٭B&*4dE)EnE![Ir!:f5`wCF@8~(`,ElZ*]6ΈРwL^4U]`TqvcP v{DWL E=C?P_Taҡ` + қ. Ln WH.r).vVbĺp+v%xL ֛. LKJQ N5`A9UaAn @WdHootA41ƶb~୰]4Hp<yˍU]|Aeҷto Jj!z Z T{j˾Dܸli/pऔ"+siyg.GEN"3!m$9Nw6b E)SJs|8L@.Uifp\Ml8WF^96C VXHsW>r(a]fgXAr ' n+w[gA%ENTJwq" +bdh Bm߈AJsM09\y@^C~ET=txd4f@ ־PLb1%ayZ"6Aȥni?X(RGCrbU1W>C1)Aarr_g7gxS𧟏O?#ž~?G<W y_)>Avdsvԯ?3g <&xL3g <&xL3g <&xL3g <&xL3g <&xL3g <&xL3g <&xL3g <&xL3g <&xL3g $\o6t$&6CUׯ^zׯ^zׯ^zׯ^zׯ^zׯ^zׯ^zׯL yOϼg3}mHzy.8H/"Q\1b :Ci'! tURqPjU5ـ ttl_seconds:flags.1?int = InputMedia; inputMediaPhoto#b3ba0635 flags:# spoiler:flags.1?true id:InputPhoto ttl_seconds:flags.0?int = InputMedia; inputMediaGeoPoint#f9c44144 geo_point:InputGeoPoint = InputMedia; inputMediaContact#f8ab7dfb phone_number:string first_name:string last_name:string vcard:string = InputMedia; inputMediaUploadedDocument#37c9330 flags:# nosound_video:flags.3?true force_file:flags.4?true spoiler:flags.5?true file:InputFile thumb:flags.2?InputFile mime_type:string attributes:Vector stickers:flags.0?Vector video_cover:flags.6?InputPhoto video_timestamp:flags.7?int ttl_seconds:flags.1?int = InputMedia; inputMediaDocument#a8763ab5 flags:# spoiler:flags.2?true id:InputDocument video_cover:flags.3?InputPhoto video_timestamp:flags.4?int ttl_seconds:flags.0?int query:flags.1?string = InputMedia; inputMediaVenue#c13d1c11 geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string = InputMedia; inputMediaPhotoExternal#e5bbfe1a flags:# spoiler:flags.1?true url:string ttl_seconds:flags.0?int = InputMedia; inputMediaDocumentExternal#779600f9 flags:# spoiler:flags.1?true url:string ttl_seconds:flags.0?int video_cover:flags.2?InputPhoto video_timestamp:flags.3?int = InputMedia; inputMediaGame#d33f43f3 id:InputGame = InputMedia; inputMediaInvoice#405fef0d flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:flags.3?string provider_data:DataJSON start_param:flags.1?string extended_media:flags.2?InputMedia = InputMedia; inputMediaGeoLive#971fa843 flags:# stopped:flags.0?true geo_point:InputGeoPoint heading:flags.2?int period:flags.1?int proximity_notification_radius:flags.3?int = InputMedia; inputMediaPoll#f94e5f1 flags:# poll:Poll correct_answers:flags.0?Vector solution:flags.1?string solution_entities:flags.1?Vector = InputMedia; inputMediaDice#e66fbf7b emoticon:string = InputMedia; inputMediaStory#89fdd778 peer:InputPeer id:int = InputMedia; inputMediaWebPage#c21b8849 flags:# force_large_media:flags.0?true force_small_media:flags.1?true optional:flags.2?true url:string = InputMedia; inputMediaPaidMedia#c4103386 flags:# stars_amount:long extended_media:Vector payload:flags.0?string = InputMedia; inputChatPhotoEmpty#1ca48f57 = InputChatPhoto; inputChatUploadedPhoto#bdcdaec0 flags:# file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double video_emoji_markup:flags.3?VideoSize = InputChatPhoto; inputChatPhoto#8953ad37 id:InputPhoto = InputChatPhoto; inputGeoPointEmpty#e4c123d6 = InputGeoPoint; inputGeoPoint#48222faf flags:# lat:double long:double accuracy_radius:flags.0?int = InputGeoPoint; inputPhotoEmpty#1cd7bf0d = InputPhoto; inputPhoto#3bb3b94a id:long access_hash:long file_reference:bytes = InputPhoto; inputFileLocation#dfdaabe1 volume_id:long local_id:int secret:long file_reference:bytes = InputFileLocation; inputEncryptedFileLocation#f5235d55 id:long access_hash:long = InputFileLocation; inputDocumentFileLocation#bad07584 id:long access_hash:long file_reference:bytes thumb_size:string = InputFileLocation; inputSecureFileLocation#cbc7ee28 id:long access_hash:long = InputFileLocation; inputTakeoutFileLocation#29be5899 = InputFileLocation; inputPhotoFileLocation#40181ffe id:long access_hash:long file_reference:bytes thumb_size:string = InputFileLocation; inputPhotoLegacyFileLocation#d83466f3 id:long access_hash:long file_reference:bytes volume_id:long local_id:int secret:long = InputFileLocation; inputPeerPhotoFileLocation#37257e99 flags:# big:flags.0?true peer:InputPeer photo_id:long = InputFileLocation; inputStickerSetThumb#9d84f3db stickerset:InputStickerSet thumb_version:int = InputFileLocation; inputGroupCallStream#598a92a flags:# call:InputGroupCall time_ms:long scale:int video_channel:flags.0?int video_quality:flags.0?int = InputFileLocation; peerUser#59511722 user_id:long = Peer; peerChat#36c6019a chat_id:long = Peer; peerChannel#a2a5371e channel_id:long = Peer; storage.fileUnknown#aa963b05 = storage.FileType; storage.filePartial#40bc6f52 = storage.FileType; storage.fileJpeg#7efe0e = storage.FileType; storage.fileGif#cae1aadf = storage.FileType; storage.filePng#a4f63c0 = storage.FileType; storage.filePdf#ae1e508d = storage.FileType; storage.fileMp3#528a0677 = storage.FileType; storage.fileMov#4b09ebbc = storage.FileType; storage.fileMp4#b3cea0e4 = storage.FileType; storage.fileWebp#1081464c = storage.FileType; userEmpty#d3bc4b7a id:long = User; user#4b46c37e flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true bot_attach_menu:flags.27?true premium:flags.28?true attach_menu_enabled:flags.29?true flags2:# bot_can_edit:flags2.1?true close_friend:flags2.2?true stories_hidden:flags2.3?true stories_unavailable:flags2.4?true contact_require_premium:flags2.10?true bot_business:flags2.11?true bot_has_main_app:flags2.13?true id:long access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector stories_max_id:flags2.5?int color:flags2.8?PeerColor profile_color:flags2.9?PeerColor bot_active_users:flags2.12?int bot_verification_icon:flags2.14?long = User; userProfilePhotoEmpty#4f11bae1 = UserProfilePhoto; userProfilePhoto#82d1f706 flags:# has_video:flags.0?true personal:flags.2?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = UserProfilePhoto; userStatusEmpty#9d05049 = UserStatus; userStatusOnline#edb93949 expires:int = UserStatus; userStatusOffline#8c703f was_online:int = UserStatus; userStatusRecently#7b197dc8 flags:# by_me:flags.0?true = UserStatus; userStatusLastWeek#541a1d1a flags:# by_me:flags.0?true = UserStatus; userStatusLastMonth#65899777 flags:# by_me:flags.0?true = UserStatus; chatEmpty#29562865 id:long = Chat; chat#41cbf256 flags:# creator:flags.0?true left:flags.2?true deactivated:flags.5?true call_active:flags.23?true call_not_empty:flags.24?true noforwards:flags.25?true id:long title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel admin_rights:flags.14?ChatAdminRights default_banned_rights:flags.18?ChatBannedRights = Chat; chatForbidden#6592a1a7 id:long title:string = Chat; channel#e00998b7 flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true join_to_send:flags.28?true join_request:flags.29?true forum:flags.30?true flags2:# stories_hidden:flags2.1?true stories_hidden_min:flags2.2?true stories_unavailable:flags2.3?true signature_profiles:flags2.12?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int restriction_reason:flags.9?Vector admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int usernames:flags2.0?Vector stories_max_id:flags2.4?int color:flags2.7?PeerColor profile_color:flags2.8?PeerColor emoji_status:flags2.9?EmojiStatus level:flags2.10?int subscription_until_date:flags2.11?int bot_verification_icon:flags2.13?long = Chat; channelForbidden#17d493d5 flags:# broadcast:flags.5?true megagroup:flags.8?true id:long access_hash:long title:string until_date:flags.16?int = Chat; chatFull#2633421b flags:# can_set_username:flags.7?true has_scheduled:flags.8?true translations_disabled:flags.19?true id:long about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer theme_emoticon:flags.16?string requests_pending:flags.17?int recent_requesters:flags.17?Vector available_reactions:flags.18?ChatReactions reactions_limit:flags.20?int = ChatFull; channelFull#52d6806b flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true stories_pinned_available:flags2.5?true view_forum_as_messages:flags2.6?true restricted_sponsored:flags2.11?true can_view_revenue:flags2.12?true paid_media_allowed:flags2.14?true can_view_stars_revenue:flags2.15?true paid_reactions_available:flags2.16?true stargifts_available:flags2.19?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions reactions_limit:flags2.13?int stories:flags2.4?PeerStories wallpaper:flags2.7?WallPaper boosts_applied:flags2.8?int boosts_unrestrict:flags2.9?int emojiset:flags2.10?StickerSet bot_verification:flags2.17?BotVerification stargifts_count:flags2.18?int = ChatFull; chatParticipant#c02d4007 user_id:long inviter_id:long date:int = ChatParticipant; chatParticipantCreator#e46bcee4 user_id:long = ChatParticipant; chatParticipantAdmin#a0933f5b user_id:long inviter_id:long date:int = ChatParticipant; chatParticipantsForbidden#8763d3e1 flags:# chat_id:long self_participant:flags.0?ChatParticipant = ChatParticipants; chatParticipants#3cbc93f8 chat_id:long participants:Vector version:int = ChatParticipants; chatPhotoEmpty#37c1011c = ChatPhoto; chatPhoto#1c6e1c11 flags:# has_video:flags.0?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = ChatPhoto; messageEmpty#90a6ca84 flags:# id:int peer_id:flags.0?Peer = Message; message#96fdbbe9 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true invert_media:flags.27?true flags2:# offline:flags2.1?true video_processing_pending:flags2.4?true id:int from_id:flags.8?Peer from_boosts_applied:flags.29?int peer_id:Peer saved_peer_id:flags.28?Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long via_business_bot_id:flags2.0?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector ttl_period:flags.25?int quick_reply_shortcut_id:flags.30?int effect:flags2.2?long factcheck:flags2.3?FactCheck report_delivery_until_date:flags2.5?int = Message; messageService#d3d28540 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true reactions_are_possible:flags.9?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?Peer peer_id:Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction reactions:flags.20?MessageReactions ttl_period:flags.25?int = Message; messageMediaEmpty#3ded6320 = MessageMedia; messageMediaPhoto#695150d7 flags:# spoiler:flags.3?true photo:flags.0?Photo ttl_seconds:flags.2?int = MessageMedia; messageMediaGeo#56e0d474 geo:GeoPoint = MessageMedia; messageMediaContact#70322949 phone_number:string first_name:string last_name:string vcard:string user_id:long = MessageMedia; messageMediaUnsupported#9f84f49e = MessageMedia; messageMediaDocument#52d8ccd9 flags:# nopremium:flags.3?true spoiler:flags.4?true video:flags.6?true round:flags.7?true voice:flags.8?true document:flags.0?Document alt_documents:flags.5?Vector video_cover:flags.9?Photo video_timestamp:flags.10?int ttl_seconds:flags.2?int = MessageMedia; messageMediaWebPage#ddf10c3b flags:# force_large_media:flags.0?true force_small_media:flags.1?true manual:flags.3?true safe:flags.4?true webpage:WebPage = MessageMedia; messageMediaVenue#2ec0533f geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string = MessageMedia; messageMediaGame#fdb19008 game:Game = MessageMedia; messageMediaInvoice#f6a548d3 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 extended_media:flags.4?MessageExtendedMedia = MessageMedia; messageMediaGeoLive#b940c666 flags:# geo:GeoPoint heading:flags.0?int period:int proximity_notification_radius:flags.1?int = MessageMedia; messageMediaPoll#4bd6e798 poll:Poll results:PollResults = MessageMedia; messageMediaDice#3f7ee58b value:int emoticon:string = MessageMedia; messageMediaStory#68cb6283 flags:# via_mention:flags.1?true peer:Peer id:int story:flags.0?StoryItem = MessageMedia; messageMediaGiveaway#aa073beb flags:# only_new_subscribers:flags.0?true winners_are_visible:flags.2?true channels:Vector countries_iso2:flags.1?Vector prize_description:flags.3?string quantity:int months:flags.4?int stars:flags.5?long until_date:int = MessageMedia; messageMediaGiveawayResults#ceaa3ea1 flags:# only_new_subscribers:flags.0?true refunded:flags.2?true channel_id:long additional_peers_count:flags.3?int launch_msg_id:int winners_count:int unclaimed_count:int winners:Vector months:flags.4?int stars:flags.5?long prize_description:flags.1?string until_date:int = MessageMedia; messageMediaPaidMedia#a8852491 stars_amount:long extended_media:Vector = MessageMedia; messageActionEmpty#b6aef7b0 = MessageAction; messageActionChatCreate#bd47cbad title:string users:Vector = MessageAction; messageActionChatEditTitle#b5a1ce5a title:string = MessageAction; messageActionChatEditPhoto#7fcb13a8 photo:Photo = MessageAction; messageActionChatDeletePhoto#95e3fbef = MessageAction; messageActionChatAddUser#15cefd00 users:Vector = MessageAction; messageActionChatDeleteUser#a43f30cc user_id:long = MessageAction; messageActionChatJoinedByLink#31224c3 inviter_id:long = MessageAction; messageActionChannelCreate#95d2ac92 title:string = MessageAction; messageActionChatMigrateTo#e1037f92 channel_id:long = MessageAction; messageActionChannelMigrateFrom#ea3948e9 title:string chat_id:long = MessageAction; messageActionPinMessage#94bd38ed = MessageAction; messageActionHistoryClear#9fbab604 = MessageAction; messageActionGameScore#92a72876 game_id:long score:int = MessageAction; messageActionPaymentSentMe#ffa00ccc flags:# recurring_init:flags.2?true recurring_used:flags.3?true currency:string total_amount:long payload:bytes info:flags.0?PaymentRequestedInfo shipping_option_id:flags.1?string charge:PaymentCharge subscription_until_date:flags.4?int = MessageAction; messageActionPaymentSent#c624b16e flags:# recurring_init:flags.2?true recurring_used:flags.3?true currency:string total_amount:long invoice_slug:flags.0?string subscription_until_date:flags.4?int = MessageAction; messageActionPhoneCall#80e11a7f flags:# video:flags.2?true call_id:long reason:flags.0?PhoneCallDiscardReason duration:flags.1?int = MessageAction; messageActionScreenshotTaken#4792929b = MessageAction; messageActionCustomAction#fae69f56 message:string = MessageAction; messageActionBotAllowed#c516d679 flags:# attach_menu:flags.1?true from_request:flags.3?true domain:flags.0?string app:flags.2?BotApp = MessageAction; messageActionSecureValuesSentMe#1b287353 values:Vector credentials:SecureCredentialsEncrypted = MessageAction; messageActionSecureValuesSent#d95c6154 types:Vector = MessageAction; messageActionContactSignUp#f3f25f76 = MessageAction; messageActionGeoProximityReached#98e0d697 from_id:Peer to_id:Peer distance:int = MessageAction; messageActionGroupCall#7a0d7f42 flags:# call:InputGroupCall duration:flags.0?int = MessageAction; messageActionInviteToGroupCall#502f92f7 call:InputGroupCall users:Vector = MessageAction; messageActionSetMessagesTTL#3c134d7b flags:# period:int auto_setting_from:flags.0?long = MessageAction; messageActionGroupCallScheduled#b3a07661 call:InputGroupCall schedule_date:int = MessageAction; messageActionSetChatTheme#aa786345 emoticon:string = MessageAction; messageActionChatJoinedByRequest#ebbca3cb = MessageAction; messageActionWebViewDataSentMe#47dd8079 text:string data:string = MessageAction; messageActionWebViewDataSent#b4c38cb5 text:string = MessageAction; messageActionGiftPremium#6c6274fa flags:# currency:string amount:long months:int crypto_currency:flags.0?string crypto_amount:flags.0?long message:flags.1?TextWithEntities = MessageAction; messageActionTopicCreate#d999256 flags:# title:string icon_color:int icon_emoji_id:flags.0?long = MessageAction; messageActionTopicEdit#c0944820 flags:# title:flags.0?string icon_emoji_id:flags.1?long closed:flags.2?Bool hidden:flags.3?Bool = MessageAction; messageActionSuggestProfilePhoto#57de635e photo:Photo = MessageAction; messageActionRequestedPeer#31518e9b button_id:int peers:Vector = MessageAction; messageActionSetChatWallPaper#5060a3f4 flags:# same:flags.0?true for_both:flags.1?true wallpaper:WallPaper = MessageAction; messageActionGiftCode#56d03994 flags:# via_giveaway:flags.0?true unclaimed:flags.2?true boost_peer:flags.1?Peer months:int slug:string currency:flags.2?string amount:flags.2?long crypto_currency:flags.3?string crypto_amount:flags.3?long message:flags.4?TextWithEntities = MessageAction; messageActionGiveawayLaunch#a80f51e4 flags:# stars:flags.0?long = MessageAction; messageActionGiveawayResults#87e2f155 flags:# stars:flags.0?true winners_count:int unclaimed_count:int = MessageAction; messageActionBoostApply#cc02aa6d boosts:int = MessageAction; messageActionRequestedPeerSentMe#93b31848 button_id:int peers:Vector = MessageAction; messageActionPaymentRefunded#41b3e202 flags:# peer:Peer currency:string total_amount:long payload:flags.0?bytes charge:PaymentCharge = MessageAction; messageActionGiftStars#45d5b021 flags:# currency:string amount:long stars:long crypto_currency:flags.0?string crypto_amount:flags.0?long transaction_id:flags.1?string = MessageAction; messageActionPrizeStars#b00c47a2 flags:# unclaimed:flags.0?true stars:long transaction_id:string boost_peer:Peer giveaway_msg_id:int = MessageAction; messageActionStarGift#4717e8a4 flags:# name_hidden:flags.0?true saved:flags.2?true converted:flags.3?true upgraded:flags.5?true refunded:flags.9?true can_upgrade:flags.10?true gift:StarGift message:flags.1?TextWithEntities convert_stars:flags.4?long upgrade_msg_id:flags.5?int upgrade_stars:flags.8?long from_id:flags.11?Peer peer:flags.12?Peer saved_id:flags.12?long = MessageAction; messageActionStarGiftUnique#acdfcb81 flags:# upgrade:flags.0?true transferred:flags.1?true saved:flags.2?true refunded:flags.5?true gift:StarGift can_export_at:flags.3?int transfer_stars:flags.4?long from_id:flags.6?Peer peer:flags.7?Peer saved_id:flags.7?long = MessageAction; dialog#d58a08c6 flags:# pinned:flags.2?true unread_mark:flags.3?true view_forum_as_messages:flags.6?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int unread_reactions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int ttl_period:flags.5?int = Dialog; dialogFolder#71bd134c flags:# pinned:flags.2?true folder:Folder peer:Peer top_message:int unread_muted_peers_count:int unread_unmuted_peers_count:int unread_muted_messages_count:int unread_unmuted_messages_count:int = Dialog; photoEmpty#2331b22d id:long = Photo; photo#fb197a65 flags:# has_stickers:flags.0?true id:long access_hash:long file_reference:bytes date:int sizes:Vector video_sizes:flags.1?Vector dc_id:int = Photo; photoSizeEmpty#e17e23c type:string = PhotoSize; photoSize#75c78e60 type:string w:int h:int size:int = PhotoSize; photoCachedSize#21e1ad6 type:string w:int h:int bytes:bytes = PhotoSize; photoStrippedSize#e0b0bc2e type:string bytes:bytes = PhotoSize; photoSizeProgressive#fa3efb95 type:string w:int h:int sizes:Vector = PhotoSize; photoPathSize#d8214d41 type:string bytes:bytes = PhotoSize; geoPointEmpty#1117dd5f = GeoPoint; geoPoint#b2a2f663 flags:# long:double lat:double access_hash:long accuracy_radius:flags.0?int = GeoPoint; auth.sentCode#5e002502 flags:# type:auth.SentCodeType phone_code_hash:string next_type:flags.1?auth.CodeType timeout:flags.2?int = auth.SentCode; auth.sentCodeSuccess#2390fe44 authorization:auth.Authorization = auth.SentCode; auth.authorization#2ea2c0d4 flags:# setup_password_required:flags.1?true otherwise_relogin_days:flags.1?int tmp_sessions:flags.0?int future_auth_token:flags.2?bytes user:User = auth.Authorization; auth.authorizationSignUpRequired#44747e9a flags:# terms_of_service:flags.0?help.TermsOfService = auth.Authorization; auth.exportedAuthorization#b434e2b8 id:long bytes:bytes = auth.ExportedAuthorization; inputNotifyPeer#b8bc5b0c peer:InputPeer = InputNotifyPeer; inputNotifyUsers#193b4417 = InputNotifyPeer; inputNotifyChats#4a95e84e = InputNotifyPeer; inputNotifyBroadcasts#b1db7c7e = InputNotifyPeer; inputNotifyForumTopic#5c467992 peer:InputPeer top_msg_id:int = InputNotifyPeer; inputPeerNotifySettings#cacb6ae2 flags:# show_previews:flags.0?Bool silent:flags.1?Bool mute_until:flags.2?int sound:flags.3?NotificationSound stories_muted:flags.6?Bool stories_hide_sender:flags.7?Bool stories_sound:flags.8?NotificationSound = InputPeerNotifySettings; peerNotifySettings#99622c0c flags:# show_previews:flags.0?Bool silent:flags.1?Bool mute_until:flags.2?int ios_sound:flags.3?NotificationSound android_sound:flags.4?NotificationSound other_sound:flags.5?NotificationSound stories_muted:flags.6?Bool stories_hide_sender:flags.7?Bool stories_ios_sound:flags.8?NotificationSound stories_android_sound:flags.9?NotificationSound stories_other_sound:flags.10?NotificationSound = PeerNotifySettings; peerSettings#acd66c5e flags:# report_spam:flags.0?true add_contact:flags.1?true block_contact:flags.2?true share_contact:flags.3?true need_contacts_exception:flags.4?true report_geo:flags.5?true autoarchived:flags.7?true invite_members:flags.8?true request_chat_broadcast:flags.10?true business_bot_paused:flags.11?true business_bot_can_reply:flags.12?true geo_distance:flags.6?int request_chat_title:flags.9?string request_chat_date:flags.9?int business_bot_id:flags.13?long business_bot_manage_url:flags.13?string = PeerSettings; wallPaper#a437c3ed id:long flags:# creator:flags.0?true default:flags.1?true pattern:flags.3?true dark:flags.4?true access_hash:long slug:string document:Document settings:flags.2?WallPaperSettings = WallPaper; wallPaperNoFile#e0804116 id:long flags:# default:flags.1?true dark:flags.4?true settings:flags.2?WallPaperSettings = WallPaper; inputReportReasonSpam#58dbcab8 = ReportReason; inputReportReasonViolence#1e22c78d = ReportReason; inputReportReasonPornography#2e59d922 = ReportReason; inputReportReasonChildAbuse#adf44ee3 = ReportReason; inputReportReasonOther#c1e4a2b1 = ReportReason; inputReportReasonCopyright#9b89f93a = ReportReason; inputReportReasonGeoIrrelevant#dbd4feed = ReportReason; inputReportReasonFake#f5ddd6e7 = ReportReason; inputReportReasonIllegalDrugs#a8eb2be = ReportReason; inputReportReasonPersonalDetails#9ec7863d = ReportReason; userFull#4d975bbc flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true stories_pinned_available:flags.26?true blocked_my_stories_from:flags.27?true wallpaper_overridden:flags.28?true contact_require_premium:flags.29?true read_dates_private:flags.30?true flags2:# sponsored_enabled:flags2.7?true can_view_revenue:flags2.9?true bot_can_manage_emoji_status:flags2.10?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights premium_gifts:flags.19?Vector wallpaper:flags.24?WallPaper stories:flags.25?PeerStories business_work_hours:flags2.0?BusinessWorkHours business_location:flags2.1?BusinessLocation business_greeting_message:flags2.2?BusinessGreetingMessage business_away_message:flags2.3?BusinessAwayMessage business_intro:flags2.4?BusinessIntro birthday:flags2.5?Birthday personal_channel_id:flags2.6?long personal_channel_message:flags2.6?int stargifts_count:flags2.8?int starref_program:flags2.11?StarRefProgram bot_verification:flags2.12?BotVerification = UserFull; contact#145ade0b user_id:long mutual:Bool = Contact; importedContact#c13e3c50 user_id:long client_id:long = ImportedContact; contactStatus#16d9703b user_id:long status:UserStatus = ContactStatus; contacts.contactsNotModified#b74ba9d2 = contacts.Contacts; contacts.contacts#eae87e42 contacts:Vector saved_count:int users:Vector = contacts.Contacts; contacts.importedContacts#77d01c3b imported:Vector popular_invites:Vector retry_contacts:Vector users:Vector = contacts.ImportedContacts; contacts.blocked#ade1591 blocked:Vector chats:Vector users:Vector = contacts.Blocked; contacts.blockedSlice#e1664194 count:int blocked:Vector chats:Vector users:Vector = contacts.Blocked; messages.dialogs#15ba6c40 dialogs:Vector messages:Vector chats:Vector users:Vector = messages.Dialogs; messages.dialogsSlice#71e094f3 count:int dialogs:Vector messages:Vector chats:Vector users:Vector = messages.Dialogs; messages.dialogsNotModified#f0e3e596 count:int = messages.Dialogs; messages.messages#8c718e87 messages:Vector chats:Vector users:Vector = messages.Messages; messages.messagesSlice#3a54685e flags:# inexact:flags.1?true count:int next_rate:flags.0?int offset_id_offset:flags.2?int messages:Vector chats:Vector users:Vector = messages.Messages; messages.channelMessages#c776ba4e flags:# inexact:flags.1?true pts:int count:int offset_id_offset:flags.2?int messages:Vector topics:Vector chats:Vector users:Vector = messages.Messages; messages.messagesNotModified#74535f21 count:int = messages.Messages; messages.chats#64ff9fd5 chats:Vector = messages.Chats; messages.chatsSlice#9cd81144 count:int chats:Vector = messages.Chats; messages.chatFull#e5d7d19c full_chat:ChatFull chats:Vector users:Vector = messages.ChatFull; messages.affectedHistory#b45c69d1 pts:int pts_count:int offset:int = messages.AffectedHistory; inputMessagesFilterEmpty#57e2f66c = MessagesFilter; inputMessagesFilterPhotos#9609a51c = MessagesFilter; inputMessagesFilterVideo#9fc00e65 = MessagesFilter; inputMessagesFilterPhotoVideo#56e9f0e4 = MessagesFilter; inputMessagesFilterDocument#9eddf188 = MessagesFilter; inputMessagesFilterUrl#7ef0dd87 = MessagesFilter; inputMessagesFilterGif#ffc86587 = MessagesFilter; inputMessagesFilterVoice#50f5c392 = MessagesFilter; inputMessagesFilterMusic#3751b49e = MessagesFilter; inputMessagesFilterChatPhotos#3a20ecb8 = MessagesFilter; inputMessagesFilterPhoneCalls#80c99768 flags:# missed:flags.0?true = MessagesFilter; inputMessagesFilterRoundVoice#7a7c17a4 = MessagesFilter; inputMessagesFilterRoundVideo#b549da53 = MessagesFilter; inputMessagesFilterMyMentions#c1f8e69a = MessagesFilter; inputMessagesFilterGeo#e7026d0d = MessagesFilter; inputMessagesFilterContacts#e062db83 = MessagesFilter; inputMessagesFilterPinned#1bb00451 = MessagesFilter; updateNewMessage#1f2b0afd message:Message pts:int pts_count:int = Update; updateMessageID#4e90bfd6 id:int random_id:long = Update; updateDeleteMessages#a20db0e5 messages:Vector pts:int pts_count:int = Update; updateUserTyping#c01e857f user_id:long action:SendMessageAction = Update; updateChatUserTyping#83487af0 chat_id:long from_id:Peer action:SendMessageAction = Update; updateChatParticipants#7761198 participants:ChatParticipants = Update; updateUserStatus#e5bdf8de user_id:long status:UserStatus = Update; updateUserName#a7848924 user_id:long first_name:string last_name:string usernames:Vector = Update; updateNewAuthorization#8951abef flags:# unconfirmed:flags.0?true hash:long date:flags.0?int device:flags.0?string location:flags.0?string = Update; updateNewEncryptedMessage#12bcbd9a message:EncryptedMessage qts:int = Update; updateEncryptedChatTyping#1710f156 chat_id:int = Update; updateEncryption#b4a2e88d chat:EncryptedChat date:int = Update; updateEncryptedMessagesRead#38fe25b7 chat_id:int max_date:int date:int = Update; updateChatParticipantAdd#3dda5451 chat_id:long user_id:long inviter_id:long date:int version:int = Update; updateChatParticipantDelete#e32f3d77 chat_id:long user_id:long version:int = Update; updateDcOptions#8e5e9873 dc_options:Vector = Update; updateNotifySettings#bec268ef peer:NotifyPeer notify_settings:PeerNotifySettings = Update; updateServiceNotification#ebe46819 flags:# popup:flags.0?true invert_media:flags.2?true inbox_date:flags.1?int type:string message:string media:MessageMedia entities:Vector = Update; updatePrivacy#ee3b272a key:PrivacyKey rules:Vector = Update; updateUserPhone#5492a13 user_id:long phone:string = Update; updateReadHistoryInbox#9c974fdf flags:# folder_id:flags.0?int peer:Peer max_id:int still_unread_count:int pts:int pts_count:int = Update; updateReadHistoryOutbox#2f2f21bf peer:Peer max_id:int pts:int pts_count:int = Update; updateWebPage#7f891213 webpage:WebPage pts:int pts_count:int = Update; updateReadMessagesContents#f8227181 flags:# messages:Vector pts:int pts_count:int date:flags.0?int = Update; updateChannelTooLong#108d941f flags:# channel_id:long pts:flags.0?int = Update; updateChannel#635b4c09 channel_id:long = Update; updateNewChannelMessage#62ba04d9 message:Message pts:int pts_count:int = Update; updateReadChannelInbox#922e6e10 flags:# folder_id:flags.0?int channel_id:long max_id:int still_unread_count:int pts:int = Update; updateDeleteChannelMessages#c32d5b12 channel_id:long messages:Vector pts:int pts_count:int = Update; updateChannelMessageViews#f226ac08 channel_id:long id:int views:int = Update; updateChatParticipantAdmin#d7ca61a2 chat_id:long user_id:long is_admin:Bool version:int = Update; updateNewStickerSet#688a30aa stickerset:messages.StickerSet = Update; updateStickerSetsOrder#bb2d201 flags:# masks:flags.0?true emojis:flags.1?true order:Vector = Update; updateStickerSets#31c24808 flags:# masks:flags.0?true emojis:flags.1?true = Update; updateSavedGifs#9375341e = Update; updateBotInlineQuery#496f379c flags:# query_id:long user_id:long query:string geo:flags.0?GeoPoint peer_type:flags.1?InlineQueryPeerType offset:string = Update; updateBotInlineSend#12f12a07 flags:# user_id:long query:string geo:flags.0?GeoPoint id:string msg_id:flags.1?InputBotInlineMessageID = Update; updateEditChannelMessage#1b3f4df7 message:Message pts:int pts_count:int = Update; updateBotCallbackQuery#b9cfc48d flags:# query_id:long user_id:long peer:Peer msg_id:int chat_instance:long data:flags.0?bytes game_short_name:flags.1?string = Update; updateEditMessage#e40370a3 message:Message pts:int pts_count:int = Update; updateInlineBotCallbackQuery#691e9052 flags:# query_id:long user_id:long msg_id:InputBotInlineMessageID chat_instance:long data:flags.0?bytes game_short_name:flags.1?string = Update; updateReadChannelOutbox#b75f99a9 channel_id:long max_id:int = Update; updateDraftMessage#1b49ec6d flags:# peer:Peer top_msg_id:flags.0?int draft:DraftMessage = Update; updateReadFeaturedStickers#571d2742 = Update; updateRecentStickers#9a422c20 = Update; updateConfig#a229dd06 = Update; updatePtsChanged#3354678f = Update; updateChannelWebPage#2f2ba99f channel_id:long webpage:WebPage pts:int pts_count:int = Update; updateDialogPinned#6e6fe51c flags:# pinned:flags.0?true folder_id:flags.1?int peer:DialogPeer = Update; updatePinnedDialogs#fa0f3ca2 flags:# folder_id:flags.1?int order:flags.0?Vector = Update; updateBotWebhookJSON#8317c0c3 data:DataJSON = Update; updateBotWebhookJSONQuery#9b9240a6 query_id:long data:DataJSON timeout:int = Update; updateBotShippingQuery#b5aefd7d query_id:long user_id:long payload:bytes shipping_address:PostAddress = Update; updateBotPrecheckoutQuery#8caa9a96 flags:# query_id:long user_id:long payload:bytes info:flags.0?PaymentRequestedInfo shipping_option_id:flags.1?string currency:string total_amount:long = Update; updatePhoneCall#ab0f6b1e phone_call:PhoneCall = Update; updateLangPackTooLong#46560264 lang_code:string = Update; updateLangPack#56022f4d difference:LangPackDifference = Update; updateFavedStickers#e511996d = Update; updateChannelReadMessagesContents#ea29055d flags:# channel_id:long top_msg_id:flags.0?int messages:Vector = Update; updateContactsReset#7084a7be = Update; updateChannelAvailableMessages#b23fc698 channel_id:long available_min_id:int = Update; updateDialogUnreadMark#e16459c3 flags:# unread:flags.0?true peer:DialogPeer = Update; updateMessagePoll#aca1657b flags:# poll_id:long poll:flags.0?Poll results:PollResults = Update; updateChatDefaultBannedRights#54c01850 peer:Peer default_banned_rights:ChatBannedRights version:int = Update; updateFolderPeers#19360dc0 folder_peers:Vector pts:int pts_count:int = Update; updatePeerSettings#6a7e7366 peer:Peer settings:PeerSettings = Update; updatePeerLocated#b4afcfb0 peers:Vector = Update; updateNewScheduledMessage#39a51dfb message:Message = Update; updateDeleteScheduledMessages#f2a71983 flags:# peer:Peer messages:Vector sent_messages:flags.0?Vector = Update; updateTheme#8216fba3 theme:Theme = Update; updateGeoLiveViewed#871fb939 peer:Peer msg_id:int = Update; updateLoginToken#564fe691 = Update; updateMessagePollVote#24f40e77 poll_id:long peer:Peer options:Vector qts:int = Update; updateDialogFilter#26ffde7d flags:# id:int filter:flags.0?DialogFilter = Update; updateDialogFilterOrder#a5d72105 order:Vector = Update; updateDialogFilters#3504914f = Update; updatePhoneCallSignalingData#2661bf09 phone_call_id:long data:bytes = Update; updateChannelMessageForwards#d29a27f4 channel_id:long id:int forwards:int = Update; updateReadChannelDiscussionInbox#d6b19546 flags:# channel_id:long top_msg_id:int read_max_id:int broadcast_id:flags.0?long broadcast_post:flags.0?int = Update; updateReadChannelDiscussionOutbox#695c9e7c channel_id:long top_msg_id:int read_max_id:int = Update; updatePeerBlocked#ebe07752 flags:# blocked:flags.0?true blocked_my_stories_from:flags.1?true peer_id:Peer = Update; updateChannelUserTyping#8c88c923 flags:# channel_id:long top_msg_id:flags.0?int from_id:Peer action:SendMessageAction = Update; updatePinnedMessages#ed85eab5 flags:# pinned:flags.0?true peer:Peer messages:Vector pts:int pts_count:int = Update; updatePinnedChannelMessages#5bb98608 flags:# pinned:flags.0?true channel_id:long messages:Vector pts:int pts_count:int = Update; updateChat#f89a6a4e chat_id:long = Update; updateGroupCallParticipants#f2ebdb4e call:InputGroupCall participants:Vector version:int = Update; updateGroupCall#97d64341 flags:# chat_id:flags.0?long call:GroupCall = Update; updatePeerHistoryTTL#bb9bb9a5 flags:# peer:Peer ttl_period:flags.0?int = Update; updateChatParticipant#d087663a flags:# chat_id:long date:int actor_id:long user_id:long prev_participant:flags.0?ChatParticipant new_participant:flags.1?ChatParticipant invite:flags.2?ExportedChatInvite qts:int = Update; updateChannelParticipant#985d3abb flags:# via_chatlist:flags.3?true channel_id:long date:int actor_id:long user_id:long prev_participant:flags.0?ChannelParticipant new_participant:flags.1?ChannelParticipant invite:flags.2?ExportedChatInvite qts:int = Update; updateBotStopped#c4870a49 user_id:long date:int stopped:Bool qts:int = Update; updateGroupCallConnection#b783982 flags:# presentation:flags.0?true params:DataJSON = Update; updateBotCommands#4d712f2e peer:Peer bot_id:long commands:Vector = Update; updatePendingJoinRequests#7063c3db peer:Peer requests_pending:int recent_requesters:Vector = Update; updateBotChatInviteRequester#11dfa986 peer:Peer date:int user_id:long about:string invite:ExportedChatInvite qts:int = Update; updateMessageReactions#5e1b3cb8 flags:# peer:Peer msg_id:int top_msg_id:flags.0?int reactions:MessageReactions = Update; updateAttachMenuBots#17b7a20b = Update; updateWebViewResultSent#1592b79d query_id:long = Update; updateBotMenuButton#14b85813 bot_id:long button:BotMenuButton = Update; updateSavedRingtones#74d8be99 = Update; updateTranscribedAudio#84cd5a flags:# pending:flags.0?true peer:Peer msg_id:int transcription_id:long text:string = Update; updateReadFeaturedEmojiStickers#fb4c496c = Update; updateUserEmojiStatus#28373599 user_id:long emoji_status:EmojiStatus = Update; updateRecentEmojiStatuses#30f443db = Update; updateRecentReactions#6f7863f4 = Update; updateMoveStickerSetToTop#86fccf85 flags:# masks:flags.0?true emojis:flags.1?true stickerset:long = Update; updateMessageExtendedMedia#d5a41724 peer:Peer msg_id:int extended_media:Vector = Update; updateChannelPinnedTopic#192efbe3 flags:# pinned:flags.0?true channel_id:long topic_id:int = Update; updateChannelPinnedTopics#fe198602 flags:# channel_id:long order:flags.0?Vector = Update; updateUser#20529438 user_id:long = Update; updateAutoSaveSettings#ec05b097 = Update; updateStory#75b3b798 peer:Peer story:StoryItem = Update; updateReadStories#f74e932b peer:Peer max_id:int = Update; updateStoryID#1bf335b9 id:int random_id:long = Update; updateStoriesStealthMode#2c084dc1 stealth_mode:StoriesStealthMode = Update; updateSentStoryReaction#7d627683 peer:Peer story_id:int reaction:Reaction = Update; updateBotChatBoost#904dd49c peer:Peer boost:Boost qts:int = Update; updateChannelViewForumAsMessages#7b68920 channel_id:long enabled:Bool = Update; updatePeerWallpaper#ae3f101d flags:# wallpaper_overridden:flags.1?true peer:Peer wallpaper:flags.0?WallPaper = Update; updateBotMessageReaction#ac21d3ce peer:Peer msg_id:int date:int actor:Peer old_reactions:Vector new_reactions:Vector qts:int = Update; updateBotMessageReactions#9cb7759 peer:Peer msg_id:int date:int reactions:Vector qts:int = Update; updateSavedDialogPinned#aeaf9e74 flags:# pinned:flags.0?true peer:DialogPeer = Update; updatePinnedSavedDialogs#686c85a6 flags:# order:flags.0?Vector = Update; updateSavedReactionTags#39c67432 = Update; updateSmsJob#f16269d4 job_id:string = Update; updateQuickReplies#f9470ab2 quick_replies:Vector = Update; updateNewQuickReply#f53da717 quick_reply:QuickReply = Update; updateDeleteQuickReply#53e6f1ec shortcut_id:int = Update; updateQuickReplyMessage#3e050d0f message:Message = Update; updateDeleteQuickReplyMessages#566fe7cd shortcut_id:int messages:Vector = Update; updateBotBusinessConnect#8ae5c97a connection:BotBusinessConnection qts:int = Update; updateBotNewBusinessMessage#9ddb347c flags:# connection_id:string message:Message reply_to_message:flags.0?Message qts:int = Update; updateBotEditBusinessMessage#7df587c flags:# connection_id:string message:Message reply_to_message:flags.0?Message qts:int = Update; updateBotDeleteBusinessMessage#a02a982e connection_id:string peer:Peer messages:Vector qts:int = Update; updateNewStoryReaction#1824e40b story_id:int peer:Peer reaction:Reaction = Update; updateBroadcastRevenueTransactions#dfd961f5 peer:Peer balances:BroadcastRevenueBalances = Update; updateStarsBalance#4e80a379 balance:StarsAmount = Update; updateBusinessBotCallbackQuery#1ea2fda7 flags:# query_id:long user_id:long connection_id:string message:Message reply_to_message:flags.2?Message chat_instance:long data:flags.0?bytes = Update; updateStarsRevenueStatus#a584b019 peer:Peer status:StarsRevenueStatus = Update; updateBotPurchasedPaidMedia#283bd312 user_id:long payload:string qts:int = Update; updatePaidReactionPrivacy#8b725fce private:PaidReactionPrivacy = Update; updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; updates.differenceEmpty#5d75a138 date:int seq:int = updates.Difference; updates.difference#f49ca0 new_messages:Vector new_encrypted_messages:Vector other_updates:Vector chats:Vector users:Vector state:updates.State = updates.Difference; updates.differenceSlice#a8fb1981 new_messages:Vector new_encrypted_messages:Vector other_updates:Vector chats:Vector users:Vector intermediate_state:updates.State = updates.Difference; updates.differenceTooLong#4afe8f6d pts:int = updates.Difference; updatesTooLong#e317af7e = Updates; updateShortMessage#313bc7f8 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true id:int user_id:long message:string pts:int pts_count:int date:int fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long reply_to:flags.3?MessageReplyHeader entities:flags.7?Vector ttl_period:flags.25?int = Updates; updateShortChatMessage#4d6deea5 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true id:int from_id:long chat_id:long message:string pts:int pts_count:int date:int fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long reply_to:flags.3?MessageReplyHeader entities:flags.7?Vector ttl_period:flags.25?int = Updates; updateShort#78d4dec1 update:Update date:int = Updates; updatesCombined#725b04c3 updates:Vector users:Vector chats:Vector date:int seq_start:int seq:int = Updates; updates#74ae4240 updates:Vector users:Vector chats:Vector date:int seq:int = Updates; updateShortSentMessage#9015e101 flags:# out:flags.1?true id:int pts:int pts_count:int date:int media:flags.9?MessageMedia entities:flags.7?Vector ttl_period:flags.25?int = Updates; photos.photos#8dca6aa5 photos:Vector users:Vector = photos.Photos; photos.photosSlice#15051f54 count:int photos:Vector users:Vector = photos.Photos; photos.photo#20212ca8 photo:Photo users:Vector = photos.Photo; upload.file#96a18d5 type:storage.FileType mtime:int bytes:bytes = upload.File; upload.fileCdnRedirect#f18cda44 dc_id:int file_token:bytes encryption_key:bytes encryption_iv:bytes file_hashes:Vector = upload.File; dcOption#18b7a10d flags:# ipv6:flags.0?true media_only:flags.1?true tcpo_only:flags.2?true cdn:flags.3?true static:flags.4?true this_port_only:flags.5?true id:int ip_address:string port:int secret:flags.10?bytes = DcOption; config#cc1a241e flags:# default_p2p_contacts:flags.3?true preload_featured_stickers:flags.4?true revoke_pm_inbox:flags.6?true blocked_mode:flags.8?true force_try_ipv6:flags.14?true date:int expires:int test_mode:Bool this_dc:int dc_options:Vector dc_txt_domain_name:string 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 push_chat_period_ms:int push_chat_limit:int edit_time_limit:int revoke_time_limit:int revoke_pm_time_limit:int rating_e_decay:int stickers_recent_limit:int channels_read_media_period:int tmp_sessions:flags.0?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 autoupdate_url_prefix:flags.7?string gif_search_username:flags.9?string venue_search_username:flags.10?string img_search_username:flags.11?string static_maps_provider:flags.12?string caption_length_max:int message_length_max:int webfile_dc_id:int suggested_lang_code:flags.2?string lang_pack_version:flags.2?int base_lang_pack_version:flags.2?int reactions_default:flags.15?Reaction autologin_token:flags.16?string = Config; nearestDc#8e1a1775 country:string this_dc:int nearest_dc:int = NearestDc; help.appUpdate#ccbbce30 flags:# can_not_skip:flags.0?true id:int version:string text:string entities:Vector document:flags.1?Document url:flags.2?string sticker:flags.3?Document = help.AppUpdate; help.noAppUpdate#c45a6536 = help.AppUpdate; help.inviteText#18cb9f78 message:string = help.InviteText; encryptedChatEmpty#ab7ec0a0 id:int = EncryptedChat; encryptedChatWaiting#66b25953 id:int access_hash:long date:int admin_id:long participant_id:long = EncryptedChat; encryptedChatRequested#48f1d94c flags:# folder_id:flags.0?int id:int access_hash:long date:int admin_id:long participant_id:long g_a:bytes = EncryptedChat; encryptedChat#61f0d4c7 id:int access_hash:long date:int admin_id:long participant_id:long g_a_or_b:bytes key_fingerprint:long = EncryptedChat; encryptedChatDiscarded#1e1c7c45 flags:# history_deleted:flags.0?true id:int = EncryptedChat; inputEncryptedChat#f141b5e1 chat_id:int access_hash:long = InputEncryptedChat; encryptedFileEmpty#c21f497e = EncryptedFile; encryptedFile#a8008cd8 id:long access_hash:long size:long dc_id:int key_fingerprint:int = EncryptedFile; inputEncryptedFileEmpty#1837c364 = InputEncryptedFile; inputEncryptedFileUploaded#64bd0306 id:long parts:int md5_checksum:string key_fingerprint:int = InputEncryptedFile; inputEncryptedFile#5a17b5e5 id:long access_hash:long = InputEncryptedFile; inputEncryptedFileBigUploaded#2dc173c8 id:long parts:int key_fingerprint:int = InputEncryptedFile; encryptedMessage#ed18c118 random_id:long chat_id:int date:int bytes:bytes file:EncryptedFile = EncryptedMessage; encryptedMessageService#23734b06 random_id:long chat_id:int date:int bytes:bytes = EncryptedMessage; messages.dhConfigNotModified#c0e24635 random:bytes = messages.DhConfig; messages.dhConfig#2c221edd g:int p:bytes version:int random:bytes = messages.DhConfig; messages.sentEncryptedMessage#560f8935 date:int = messages.SentEncryptedMessage; messages.sentEncryptedFile#9493ff32 date:int file:EncryptedFile = messages.SentEncryptedMessage; inputDocumentEmpty#72f0eaae = InputDocument; inputDocument#1abfb575 id:long access_hash:long file_reference:bytes = InputDocument; documentEmpty#36f8c871 id:long = Document; document#8fd4c4d8 flags:# id:long access_hash:long file_reference:bytes date:int mime_type:string size:long thumbs:flags.0?Vector video_thumbs:flags.1?Vector dc_id:int attributes:Vector = Document; help.support#17c6b5f6 phone_number:string user:User = help.Support; notifyPeer#9fd40bd8 peer:Peer = NotifyPeer; notifyUsers#b4c83b4c = NotifyPeer; notifyChats#c007cec3 = NotifyPeer; notifyBroadcasts#d612e8ef = NotifyPeer; notifyForumTopic#226e6308 peer:Peer top_msg_id:int = NotifyPeer; sendMessageTypingAction#16bf744e = SendMessageAction; sendMessageCancelAction#fd5ec8f5 = SendMessageAction; sendMessageRecordVideoAction#a187d66f = SendMessageAction; sendMessageUploadVideoAction#e9763aec progress:int = SendMessageAction; sendMessageRecordAudioAction#d52f73f7 = SendMessageAction; sendMessageUploadAudioAction#f351d7ab progress:int = SendMessageAction; sendMessageUploadPhotoAction#d1d34a26 progress:int = SendMessageAction; sendMessageUploadDocumentAction#aa0cd9e4 progress:int = SendMessageAction; sendMessageGeoLocationAction#176f8ba1 = SendMessageAction; sendMessageChooseContactAction#628cbc6f = SendMessageAction; sendMessageGamePlayAction#dd6a8f48 = SendMessageAction; sendMessageRecordRoundAction#88f27fbc = SendMessageAction; sendMessageUploadRoundAction#243e1c66 progress:int = SendMessageAction; speakingInGroupCallAction#d92c2285 = SendMessageAction; sendMessageHistoryImportAction#dbda9246 progress:int = SendMessageAction; sendMessageChooseStickerAction#b05ac6b1 = SendMessageAction; sendMessageEmojiInteraction#25972bcb emoticon:string msg_id:int interaction:DataJSON = SendMessageAction; sendMessageEmojiInteractionSeen#b665902e emoticon:string = SendMessageAction; contacts.found#b3134d9d my_results:Vector results:Vector chats:Vector users:Vector = contacts.Found; inputPrivacyKeyStatusTimestamp#4f96cb18 = InputPrivacyKey; inputPrivacyKeyChatInvite#bdfb0426 = InputPrivacyKey; inputPrivacyKeyPhoneCall#fabadc5f = InputPrivacyKey; inputPrivacyKeyPhoneP2P#db9e70d2 = InputPrivacyKey; inputPrivacyKeyForwards#a4dd4c08 = InputPrivacyKey; inputPrivacyKeyProfilePhoto#5719bacc = InputPrivacyKey; inputPrivacyKeyPhoneNumber#352dafa = InputPrivacyKey; inputPrivacyKeyAddedByPhone#d1219bdd = InputPrivacyKey; inputPrivacyKeyVoiceMessages#aee69d68 = InputPrivacyKey; inputPrivacyKeyAbout#3823cc40 = InputPrivacyKey; inputPrivacyKeyBirthday#d65a11cc = InputPrivacyKey; inputPrivacyKeyStarGiftsAutoSave#e1732341 = InputPrivacyKey; privacyKeyStatusTimestamp#bc2eab30 = PrivacyKey; privacyKeyChatInvite#500e6dfa = PrivacyKey; privacyKeyPhoneCall#3d662b7b = PrivacyKey; privacyKeyPhoneP2P#39491cc8 = PrivacyKey; privacyKeyForwards#69ec56a3 = PrivacyKey; privacyKeyProfilePhoto#96151fed = PrivacyKey; privacyKeyPhoneNumber#d19ae46d = PrivacyKey; privacyKeyAddedByPhone#42ffd42b = PrivacyKey; privacyKeyVoiceMessages#697f414 = PrivacyKey; privacyKeyAbout#a486b761 = PrivacyKey; privacyKeyBirthday#2000a518 = PrivacyKey; privacyKeyStarGiftsAutoSave#2ca4fdf8 = PrivacyKey; inputPrivacyValueAllowContacts#d09e07b = InputPrivacyRule; inputPrivacyValueAllowAll#184b35ce = InputPrivacyRule; inputPrivacyValueAllowUsers#131cc67f users:Vector = InputPrivacyRule; inputPrivacyValueDisallowContacts#ba52007 = InputPrivacyRule; inputPrivacyValueDisallowAll#d66b66c9 = InputPrivacyRule; inputPrivacyValueDisallowUsers#90110467 users:Vector = InputPrivacyRule; inputPrivacyValueAllowChatParticipants#840649cf chats:Vector = InputPrivacyRule; inputPrivacyValueDisallowChatParticipants#e94f0f86 chats:Vector = InputPrivacyRule; inputPrivacyValueAllowCloseFriends#2f453e49 = InputPrivacyRule; inputPrivacyValueAllowPremium#77cdc9f1 = InputPrivacyRule; inputPrivacyValueAllowBots#5a4fcce5 = InputPrivacyRule; inputPrivacyValueDisallowBots#c4e57915 = InputPrivacyRule; privacyValueAllowContacts#fffe1bac = PrivacyRule; privacyValueAllowAll#65427b82 = PrivacyRule; privacyValueAllowUsers#b8905fb2 users:Vector = PrivacyRule; privacyValueDisallowContacts#f888fa1a = PrivacyRule; privacyValueDisallowAll#8b73e763 = PrivacyRule; privacyValueDisallowUsers#e4621141 users:Vector = PrivacyRule; privacyValueAllowChatParticipants#6b134e8e chats:Vector = PrivacyRule; privacyValueDisallowChatParticipants#41c87565 chats:Vector = PrivacyRule; privacyValueAllowCloseFriends#f7e8d89b = PrivacyRule; privacyValueAllowPremium#ece9814b = PrivacyRule; privacyValueAllowBots#21461b5d = PrivacyRule; privacyValueDisallowBots#f6a5f82f = PrivacyRule; account.privacyRules#50a04e45 rules:Vector chats:Vector users:Vector = account.PrivacyRules; accountDaysTTL#b8d0afdf days:int = AccountDaysTTL; documentAttributeImageSize#6c37c15c w:int h:int = DocumentAttribute; documentAttributeAnimated#11b58939 = DocumentAttribute; documentAttributeSticker#6319d612 flags:# mask:flags.1?true alt:string stickerset:InputStickerSet mask_coords:flags.0?MaskCoords = DocumentAttribute; documentAttributeVideo#43c57c48 flags:# round_message:flags.0?true supports_streaming:flags.1?true nosound:flags.3?true duration:double w:int h:int preload_prefix_size:flags.2?int video_start_ts:flags.4?double video_codec:flags.5?string = DocumentAttribute; documentAttributeAudio#9852f9c6 flags:# voice:flags.10?true duration:int title:flags.0?string performer:flags.1?string waveform:flags.2?bytes = DocumentAttribute; documentAttributeFilename#15590068 file_name:string = DocumentAttribute; documentAttributeHasStickers#9801d2f7 = DocumentAttribute; documentAttributeCustomEmoji#fd149899 flags:# free:flags.0?true text_color:flags.1?true alt:string stickerset:InputStickerSet = DocumentAttribute; messages.stickersNotModified#f1749a22 = messages.Stickers; messages.stickers#30a6ec7e hash:long stickers:Vector = messages.Stickers; stickerPack#12b299d4 emoticon:string documents:Vector = StickerPack; messages.allStickersNotModified#e86602c3 = messages.AllStickers; messages.allStickers#cdbbcebb hash:long sets:Vector = messages.AllStickers; messages.affectedMessages#84d19185 pts:int pts_count:int = messages.AffectedMessages; webPageEmpty#211a1788 flags:# id:long url:flags.0?string = WebPage; webPagePending#b0d13e47 flags:# id:long url:flags.0?string date:int = WebPage; webPage#e89c45b2 flags:# has_large_media:flags.13?true video_cover_photo:flags.14?true id:long url:string display_url:string hash:int type:flags.0?string site_name:flags.1?string title:flags.2?string description:flags.3?string photo:flags.4?Photo embed_url:flags.5?string embed_type:flags.5?string embed_width:flags.6?int embed_height:flags.6?int duration:flags.7?int author:flags.8?string document:flags.9?Document cached_page:flags.10?Page attributes:flags.12?Vector = WebPage; webPageNotModified#7311ca11 flags:# cached_page_views:flags.0?int = WebPage; authorization#ad01d61d flags:# current:flags.0?true official_app:flags.1?true password_pending:flags.2?true encrypted_requests_disabled:flags.3?true call_requests_disabled:flags.4?true unconfirmed:flags.5?true hash:long device_model:string platform:string system_version:string api_id:int app_name:string app_version:string date_created:int date_active:int ip:string country:string region:string = Authorization; account.authorizations#4bff8ea0 authorization_ttl_days:int authorizations:Vector = account.Authorizations; account.password#957b50fb flags:# has_recovery:flags.0?true has_secure_values:flags.1?true has_password:flags.2?true current_algo:flags.2?PasswordKdfAlgo srp_B:flags.2?bytes srp_id:flags.2?long hint:flags.3?string email_unconfirmed_pattern:flags.4?string new_algo:PasswordKdfAlgo new_secure_algo:SecurePasswordKdfAlgo secure_random:bytes pending_reset_date:flags.5?int login_email_pattern:flags.6?string = account.Password; account.passwordSettings#9a5c33e5 flags:# email:flags.0?string secure_settings:flags.1?SecureSecretSettings = account.PasswordSettings; account.passwordInputSettings#c23727c9 flags:# new_algo:flags.0?PasswordKdfAlgo new_password_hash:flags.0?bytes hint:flags.0?string email:flags.1?string new_secure_settings:flags.2?SecureSecretSettings = account.PasswordInputSettings; auth.passwordRecovery#137948a5 email_pattern:string = auth.PasswordRecovery; receivedNotifyMessage#a384b779 id:int flags:int = ReceivedNotifyMessage; chatInviteExported#a22cbd96 flags:# revoked:flags.0?true permanent:flags.5?true request_needed:flags.6?true link:string admin_id:long date:int start_date:flags.4?int expire_date:flags.1?int usage_limit:flags.2?int usage:flags.3?int requested:flags.7?int subscription_expired:flags.10?int title:flags.8?string subscription_pricing:flags.9?StarsSubscriptionPricing = ExportedChatInvite; chatInvitePublicJoinRequests#ed107ab7 = ExportedChatInvite; chatInviteAlready#5a686d7c chat:Chat = ChatInvite; chatInvite#5c9d3702 flags:# channel:flags.0?true broadcast:flags.1?true public:flags.2?true megagroup:flags.3?true request_needed:flags.6?true verified:flags.7?true scam:flags.8?true fake:flags.9?true can_refulfill_subscription:flags.11?true title:string about:flags.5?string photo:Photo participants_count:int participants:flags.4?Vector color:int subscription_pricing:flags.10?StarsSubscriptionPricing subscription_form_id:flags.12?long bot_verification:flags.13?BotVerification = ChatInvite; chatInvitePeek#61695cb0 chat:Chat expires:int = ChatInvite; inputStickerSetEmpty#ffb62b95 = InputStickerSet; inputStickerSetID#9de7a269 id:long access_hash:long = InputStickerSet; inputStickerSetShortName#861cc8a0 short_name:string = InputStickerSet; inputStickerSetAnimatedEmoji#28703c8 = InputStickerSet; inputStickerSetDice#e67f520e emoticon:string = InputStickerSet; inputStickerSetAnimatedEmojiAnimations#cde3739 = InputStickerSet; inputStickerSetPremiumGifts#c88b3b02 = InputStickerSet; inputStickerSetEmojiGenericAnimations#4c4d4ce = InputStickerSet; inputStickerSetEmojiDefaultStatuses#29d0f5ee = InputStickerSet; inputStickerSetEmojiDefaultTopicIcons#44c1f8e9 = InputStickerSet; inputStickerSetEmojiChannelDefaultStatuses#49748553 = InputStickerSet; stickerSet#2dd14edc flags:# archived:flags.1?true official:flags.2?true masks:flags.3?true emojis:flags.7?true text_color:flags.9?true channel_emoji_status:flags.10?true creator:flags.11?true installed_date:flags.0?int id:long access_hash:long title:string short_name:string thumbs:flags.4?Vector thumb_dc_id:flags.4?int thumb_version:flags.4?int thumb_document_id:flags.8?long count:int hash:int = StickerSet; messages.stickerSet#6e153f16 set:StickerSet packs:Vector keywords:Vector documents:Vector = messages.StickerSet; messages.stickerSetNotModified#d3f924eb = messages.StickerSet; botCommand#c27ac8c7 command:string description:string = BotCommand; botInfo#4d8a0299 flags:# has_preview_medias:flags.6?true user_id:flags.0?long description:flags.1?string description_photo:flags.4?Photo description_document:flags.5?Document commands:flags.2?Vector menu_button:flags.3?BotMenuButton privacy_policy_url:flags.7?string app_settings:flags.8?BotAppSettings verifier_settings:flags.9?BotVerifierSettings = BotInfo; keyboardButton#a2fa4880 text:string = KeyboardButton; keyboardButtonUrl#258aff05 text:string url:string = KeyboardButton; keyboardButtonCallback#35bbdb6b flags:# requires_password:flags.0?true text:string data:bytes = KeyboardButton; keyboardButtonRequestPhone#b16a6c29 text:string = KeyboardButton; keyboardButtonRequestGeoLocation#fc796b3f text:string = KeyboardButton; keyboardButtonSwitchInline#93b9fbb5 flags:# same_peer:flags.0?true text:string query:string peer_types:flags.1?Vector = KeyboardButton; keyboardButtonGame#50f41ccf text:string = KeyboardButton; keyboardButtonBuy#afd93fbb text:string = KeyboardButton; keyboardButtonUrlAuth#10b78d29 flags:# text:string fwd_text:flags.0?string url:string button_id:int = KeyboardButton; inputKeyboardButtonUrlAuth#d02e7fd4 flags:# request_write_access:flags.0?true text:string fwd_text:flags.1?string url:string bot:InputUser = KeyboardButton; keyboardButtonRequestPoll#bbc7515d flags:# quiz:flags.0?Bool text:string = KeyboardButton; inputKeyboardButtonUserProfile#e988037b text:string user_id:InputUser = KeyboardButton; keyboardButtonUserProfile#308660c1 text:string user_id:long = KeyboardButton; keyboardButtonWebView#13767230 text:string url:string = KeyboardButton; keyboardButtonSimpleWebView#a0c0505c text:string url:string = KeyboardButton; keyboardButtonRequestPeer#53d7bfd8 text:string button_id:int peer_type:RequestPeerType max_quantity:int = KeyboardButton; inputKeyboardButtonRequestPeer#c9662d05 flags:# name_requested:flags.0?true username_requested:flags.1?true photo_requested:flags.2?true text:string button_id:int peer_type:RequestPeerType max_quantity:int = KeyboardButton; keyboardButtonCopy#75d2698e text:string copy_text:string = KeyboardButton; keyboardButtonRow#77608b83 buttons:Vector = KeyboardButtonRow; replyKeyboardHide#a03e5b85 flags:# selective:flags.2?true = ReplyMarkup; replyKeyboardForceReply#86b40b08 flags:# single_use:flags.1?true selective:flags.2?true placeholder:flags.3?string = ReplyMarkup; replyKeyboardMarkup#85dd99d1 flags:# resize:flags.0?true single_use:flags.1?true selective:flags.2?true persistent:flags.4?true rows:Vector placeholder:flags.3?string = ReplyMarkup; replyInlineMarkup#48a30254 rows:Vector = ReplyMarkup; messageEntityUnknown#bb92ba95 offset:int length:int = MessageEntity; messageEntityMention#fa04579d offset:int length:int = MessageEntity; messageEntityHashtag#6f635b0d offset:int length:int = MessageEntity; messageEntityBotCommand#6cef8ac7 offset:int length:int = MessageEntity; messageEntityUrl#6ed02538 offset:int length:int = MessageEntity; messageEntityEmail#64e475c2 offset:int length:int = MessageEntity; messageEntityBold#bd610bc9 offset:int length:int = MessageEntity; messageEntityItalic#826f8b60 offset:int length:int = MessageEntity; messageEntityCode#28a20571 offset:int length:int = MessageEntity; messageEntityPre#73924be0 offset:int length:int language:string = MessageEntity; messageEntityTextUrl#76a6d327 offset:int length:int url:string = MessageEntity; messageEntityMentionName#dc7b1140 offset:int length:int user_id:long = MessageEntity; inputMessageEntityMentionName#208e68c9 offset:int length:int user_id:InputUser = MessageEntity; messageEntityPhone#9b69e34b offset:int length:int = MessageEntity; messageEntityCashtag#4c4e743f offset:int length:int = MessageEntity; messageEntityUnderline#9c4e7e8b offset:int length:int = MessageEntity; messageEntityStrike#bf0693d4 offset:int length:int = MessageEntity; messageEntityBankCard#761e6af4 offset:int length:int = MessageEntity; messageEntitySpoiler#32ca960f offset:int length:int = MessageEntity; messageEntityCustomEmoji#c8cf05f8 offset:int length:int document_id:long = MessageEntity; messageEntityBlockquote#f1ccaaac flags:# collapsed:flags.0?true offset:int length:int = MessageEntity; inputChannelEmpty#ee8c1e86 = InputChannel; inputChannel#f35aec28 channel_id:long access_hash:long = InputChannel; inputChannelFromMessage#5b934f9d peer:InputPeer msg_id:int channel_id:long = InputChannel; contacts.resolvedPeer#7f077ad9 peer:Peer chats:Vector users:Vector = contacts.ResolvedPeer; messageRange#ae30253 min_id:int max_id:int = MessageRange; updates.channelDifferenceEmpty#3e11affb flags:# final:flags.0?true pts:int timeout:flags.1?int = updates.ChannelDifference; updates.channelDifferenceTooLong#a4bcc6fe flags:# final:flags.0?true timeout:flags.1?int dialog:Dialog messages:Vector chats:Vector users:Vector = updates.ChannelDifference; updates.channelDifference#2064674e flags:# final:flags.0?true pts:int timeout:flags.1?int new_messages:Vector other_updates:Vector chats:Vector users:Vector = updates.ChannelDifference; channelMessagesFilterEmpty#94d42ee7 = ChannelMessagesFilter; channelMessagesFilter#cd77d957 flags:# exclude_new_messages:flags.1?true ranges:Vector = ChannelMessagesFilter; channelParticipant#cb397619 flags:# user_id:long date:int subscription_until_date:flags.0?int = ChannelParticipant; channelParticipantSelf#4f607bef flags:# via_request:flags.0?true user_id:long inviter_id:long date:int subscription_until_date:flags.1?int = ChannelParticipant; channelParticipantCreator#2fe601d3 flags:# user_id:long admin_rights:ChatAdminRights rank:flags.0?string = ChannelParticipant; channelParticipantAdmin#34c3bb53 flags:# can_edit:flags.0?true self:flags.1?true user_id:long inviter_id:flags.1?long promoted_by:long date:int admin_rights:ChatAdminRights rank:flags.2?string = ChannelParticipant; channelParticipantBanned#6df8014e flags:# left:flags.0?true peer:Peer kicked_by:long date:int banned_rights:ChatBannedRights = ChannelParticipant; channelParticipantLeft#1b03f006 peer:Peer = ChannelParticipant; channelParticipantsRecent#de3f3c79 = ChannelParticipantsFilter; channelParticipantsAdmins#b4608969 = ChannelParticipantsFilter; channelParticipantsKicked#a3b54985 q:string = ChannelParticipantsFilter; channelParticipantsBots#b0d1865b = ChannelParticipantsFilter; channelParticipantsBanned#1427a5e1 q:string = ChannelParticipantsFilter; channelParticipantsSearch#656ac4b q:string = ChannelParticipantsFilter; channelParticipantsContacts#bb6ae88d q:string = ChannelParticipantsFilter; channelParticipantsMentions#e04b5ceb flags:# q:flags.0?string top_msg_id:flags.1?int = ChannelParticipantsFilter; channels.channelParticipants#9ab0feaf count:int participants:Vector chats:Vector users:Vector = channels.ChannelParticipants; channels.channelParticipantsNotModified#f0173fe9 = channels.ChannelParticipants; channels.channelParticipant#dfb80317 participant:ChannelParticipant chats:Vector users:Vector = channels.ChannelParticipant; help.termsOfService#780a0310 flags:# popup:flags.0?true id:DataJSON text:string entities:Vector min_age_confirm:flags.1?int = help.TermsOfService; messages.savedGifsNotModified#e8025ca2 = messages.SavedGifs; messages.savedGifs#84a02a0d hash:long gifs:Vector = messages.SavedGifs; inputBotInlineMessageMediaAuto#3380c786 flags:# invert_media:flags.3?true message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageText#3dcd7a87 flags:# no_webpage:flags.0?true invert_media:flags.3?true message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageMediaGeo#96929a85 flags:# geo_point:InputGeoPoint heading:flags.0?int period:flags.1?int proximity_notification_radius:flags.3?int reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageMediaVenue#417bbf11 flags:# geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageMediaContact#a6edbffd flags:# phone_number:string first_name:string last_name:string vcard:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageGame#4b425864 flags:# reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageMediaInvoice#d7e78225 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineMessageMediaWebPage#bddcc510 flags:# invert_media:flags.3?true force_large_media:flags.4?true force_small_media:flags.5?true optional:flags.6?true message:string entities:flags.1?Vector url:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; inputBotInlineResult#88bf9319 flags:# id:string type:string title:flags.1?string description:flags.2?string url:flags.3?string thumb:flags.4?InputWebDocument content:flags.5?InputWebDocument send_message:InputBotInlineMessage = InputBotInlineResult; inputBotInlineResultPhoto#a8d864a7 id:string type:string photo:InputPhoto send_message:InputBotInlineMessage = InputBotInlineResult; inputBotInlineResultDocument#fff8fdc4 flags:# id:string type:string title:flags.1?string description:flags.2?string document:InputDocument send_message:InputBotInlineMessage = InputBotInlineResult; inputBotInlineResultGame#4fa417f2 id:string short_name:string send_message:InputBotInlineMessage = InputBotInlineResult; botInlineMessageMediaAuto#764cf810 flags:# invert_media:flags.3?true message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineMessageText#8c7f65e2 flags:# no_webpage:flags.0?true invert_media:flags.3?true message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineMessageMediaGeo#51846fd flags:# geo:GeoPoint heading:flags.0?int period:flags.1?int proximity_notification_radius:flags.3?int reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineMessageMediaVenue#8a86659c flags:# geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineMessageMediaContact#18d1cdc2 flags:# phone_number:string first_name:string last_name:string vcard:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineMessageMediaInvoice#354a9b09 flags:# shipping_address_requested:flags.1?true test:flags.3?true title:string description:string photo:flags.0?WebDocument currency:string total_amount:long reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineMessageMediaWebPage#809ad9a6 flags:# invert_media:flags.3?true force_large_media:flags.4?true force_small_media:flags.5?true manual:flags.7?true safe:flags.8?true message:string entities:flags.1?Vector url:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage; botInlineResult#11965f3a flags:# id:string type:string title:flags.1?string description:flags.2?string url:flags.3?string thumb:flags.4?WebDocument content:flags.5?WebDocument 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#e021f2f6 flags:# gallery:flags.0?true query_id:long next_offset:flags.1?string switch_pm:flags.2?InlineBotSwitchPM switch_webview:flags.3?InlineBotWebView results:Vector cache_time:int users:Vector = messages.BotResults; exportedMessageLink#5dab1af4 link:string html:string = ExportedMessageLink; messageFwdHeader#4e4df4bb flags:# imported:flags.7?true saved_out:flags.11?true from_id:flags.0?Peer from_name:flags.5?string date:int channel_post:flags.2?int post_author:flags.3?string saved_from_peer:flags.4?Peer saved_from_msg_id:flags.4?int saved_from_id:flags.8?Peer saved_from_name:flags.9?string saved_date:flags.10?int psa_type:flags.6?string = MessageFwdHeader; auth.codeTypeSms#72a3158c = auth.CodeType; auth.codeTypeCall#741cd3e3 = auth.CodeType; auth.codeTypeFlashCall#226ccefb = auth.CodeType; auth.codeTypeMissedCall#d61ad6ee = auth.CodeType; auth.codeTypeFragmentSms#6ed998c = auth.CodeType; auth.sentCodeTypeApp#3dbb5986 length:int = auth.SentCodeType; auth.sentCodeTypeSms#c000bba2 length:int = auth.SentCodeType; auth.sentCodeTypeCall#5353e5a7 length:int = auth.SentCodeType; auth.sentCodeTypeFlashCall#ab03c6d9 pattern:string = auth.SentCodeType; auth.sentCodeTypeMissedCall#82006484 prefix:string length:int = auth.SentCodeType; auth.sentCodeTypeEmailCode#f450f59b flags:# apple_signin_allowed:flags.0?true google_signin_allowed:flags.1?true email_pattern:string length:int reset_available_period:flags.3?int reset_pending_date:flags.4?int = auth.SentCodeType; auth.sentCodeTypeSetUpEmailRequired#a5491dea flags:# apple_signin_allowed:flags.0?true google_signin_allowed:flags.1?true = auth.SentCodeType; auth.sentCodeTypeFragmentSms#d9565c39 url:string length:int = auth.SentCodeType; auth.sentCodeTypeFirebaseSms#9fd736 flags:# nonce:flags.0?bytes play_integrity_project_id:flags.2?long play_integrity_nonce:flags.2?bytes receipt:flags.1?string push_timeout:flags.1?int length:int = auth.SentCodeType; auth.sentCodeTypeSmsWord#a416ac81 flags:# beginning:flags.0?string = auth.SentCodeType; auth.sentCodeTypeSmsPhrase#b37794af flags:# beginning:flags.0?string = auth.SentCodeType; messages.botCallbackAnswer#36585ea4 flags:# alert:flags.1?true has_url:flags.3?true native_ui:flags.4?true message:flags.0?string url:flags.2?string cache_time:int = messages.BotCallbackAnswer; messages.messageEditData#26b5dde6 flags:# caption:flags.0?true = messages.MessageEditData; inputBotInlineMessageID#890c3d89 dc_id:int id:long access_hash:long = InputBotInlineMessageID; inputBotInlineMessageID64#b6d915d7 dc_id:int owner_id:long id:int access_hash:long = InputBotInlineMessageID; inlineBotSwitchPM#3c20629f text:string start_param:string = InlineBotSwitchPM; messages.peerDialogs#3371c354 dialogs:Vector messages:Vector chats:Vector users:Vector state:updates.State = messages.PeerDialogs; topPeer#edcdc05b peer:Peer rating:double = TopPeer; topPeerCategoryBotsPM#ab661b5b = TopPeerCategory; topPeerCategoryBotsInline#148677e2 = TopPeerCategory; topPeerCategoryCorrespondents#637b7ed = TopPeerCategory; topPeerCategoryGroups#bd17a14a = TopPeerCategory; topPeerCategoryChannels#161d9628 = TopPeerCategory; topPeerCategoryPhoneCalls#1e76a78c = TopPeerCategory; topPeerCategoryForwardUsers#a8406ca9 = TopPeerCategory; topPeerCategoryForwardChats#fbeec0f0 = TopPeerCategory; topPeerCategoryBotsApp#fd9e7bec = TopPeerCategory; topPeerCategoryPeers#fb834291 category:TopPeerCategory count:int peers:Vector = TopPeerCategoryPeers; contacts.topPeersNotModified#de266ef5 = contacts.TopPeers; contacts.topPeers#70b772a8 categories:Vector chats:Vector users:Vector = contacts.TopPeers; contacts.topPeersDisabled#b52c939d = contacts.TopPeers; draftMessageEmpty#1b0c841a flags:# date:flags.0?int = DraftMessage; draftMessage#2d65321f flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo message:string entities:flags.3?Vector media:flags.5?InputMedia date:int effect:flags.7?long = DraftMessage; messages.featuredStickersNotModified#c6dc0c66 count:int = messages.FeaturedStickers; messages.featuredStickers#be382906 flags:# premium:flags.0?true hash:long count:int sets:Vector unread:Vector = messages.FeaturedStickers; messages.recentStickersNotModified#b17f890 = messages.RecentStickers; messages.recentStickers#88d37c56 hash:long packs:Vector stickers:Vector dates:Vector = messages.RecentStickers; messages.archivedStickers#4fcba9c8 count:int sets:Vector = messages.ArchivedStickers; messages.stickerSetInstallResultSuccess#38641628 = messages.StickerSetInstallResult; messages.stickerSetInstallResultArchive#35e410a8 sets:Vector = messages.StickerSetInstallResult; stickerSetCovered#6410a5d2 set:StickerSet cover:Document = StickerSetCovered; stickerSetMultiCovered#3407e51b set:StickerSet covers:Vector = StickerSetCovered; stickerSetFullCovered#40d13c0e set:StickerSet packs:Vector keywords:Vector documents:Vector = StickerSetCovered; stickerSetNoCovered#77b15d1c set:StickerSet = StickerSetCovered; maskCoords#aed6dbb2 n:int x:double y:double zoom:double = MaskCoords; inputStickeredMediaPhoto#4a992157 id:InputPhoto = InputStickeredMedia; inputStickeredMediaDocument#438865b id:InputDocument = InputStickeredMedia; game#bdf9653b flags:# id:long access_hash:long short_name:string title:string description:string photo:Photo document:flags.0?Document = Game; inputGameID#32c3e77 id:long access_hash:long = InputGame; inputGameShortName#c331e80a bot_id:InputUser short_name:string = InputGame; highScore#73a379eb pos:int user_id:long score:int = HighScore; messages.highScores#9a3bfd99 scores:Vector users:Vector = messages.HighScores; textEmpty#dc3d824f = RichText; textPlain#744694e0 text:string = RichText; textBold#6724abc4 text:RichText = RichText; textItalic#d912a59c text:RichText = RichText; textUnderline#c12622c4 text:RichText = RichText; textStrike#9bf8bb95 text:RichText = RichText; textFixed#6c3f19b9 text:RichText = RichText; textUrl#3c2884c1 text:RichText url:string webpage_id:long = RichText; textEmail#de5a0dd6 text:RichText email:string = RichText; textConcat#7e6260d7 texts:Vector = RichText; textSubscript#ed6a8504 text:RichText = RichText; textSuperscript#c7fb5e01 text:RichText = RichText; textMarked#34b8621 text:RichText = RichText; textPhone#1ccb966a text:RichText phone:string = RichText; textImage#81ccf4f document_id:long w:int h:int = RichText; textAnchor#35553762 text:RichText name:string = RichText; pageBlockUnsupported#13567e8a = PageBlock; pageBlockTitle#70abc3fd text:RichText = PageBlock; pageBlockSubtitle#8ffa9a1f text:RichText = PageBlock; pageBlockAuthorDate#baafe5e0 author:RichText published_date:int = PageBlock; pageBlockHeader#bfd064ec text:RichText = PageBlock; pageBlockSubheader#f12bb6e1 text:RichText = PageBlock; pageBlockParagraph#467a0766 text:RichText = PageBlock; pageBlockPreformatted#c070d93e text:RichText language:string = PageBlock; pageBlockFooter#48870999 text:RichText = PageBlock; pageBlockDivider#db20b188 = PageBlock; pageBlockAnchor#ce0d37b0 name:string = PageBlock; pageBlockList#e4e88011 items:Vector = PageBlock; pageBlockBlockquote#263d7c26 text:RichText caption:RichText = PageBlock; pageBlockPullquote#4f4456d3 text:RichText caption:RichText = PageBlock; pageBlockPhoto#1759c560 flags:# photo_id:long caption:PageCaption url:flags.0?string webpage_id:flags.0?long = PageBlock; pageBlockVideo#7c8fe7b6 flags:# autoplay:flags.0?true loop:flags.1?true video_id:long caption:PageCaption = PageBlock; pageBlockCover#39f23300 cover:PageBlock = PageBlock; pageBlockEmbed#a8718dc5 flags:# full_width:flags.0?true allow_scrolling:flags.3?true url:flags.1?string html:flags.2?string poster_photo_id:flags.4?long w:flags.5?int h:flags.5?int caption:PageCaption = PageBlock; pageBlockEmbedPost#f259a80b url:string webpage_id:long author_photo_id:long author:string date:int blocks:Vector caption:PageCaption = PageBlock; pageBlockCollage#65a0fa4d items:Vector caption:PageCaption = PageBlock; pageBlockSlideshow#31f9590 items:Vector caption:PageCaption = PageBlock; pageBlockChannel#ef1751b5 channel:Chat = PageBlock; pageBlockAudio#804361ea audio_id:long caption:PageCaption = PageBlock; pageBlockKicker#1e148390 text:RichText = PageBlock; pageBlockTable#bf4dea82 flags:# bordered:flags.0?true striped:flags.1?true title:RichText rows:Vector = PageBlock; pageBlockOrderedList#9a8ae1e1 items:Vector = PageBlock; pageBlockDetails#76768bed flags:# open:flags.0?true blocks:Vector title:RichText = PageBlock; pageBlockRelatedArticles#16115a96 title:RichText articles:Vector = PageBlock; pageBlockMap#a44f3ef6 geo:GeoPoint zoom:int w:int h:int caption:PageCaption = PageBlock; phoneCallDiscardReasonMissed#85e42301 = PhoneCallDiscardReason; phoneCallDiscardReasonDisconnect#e095c1a0 = PhoneCallDiscardReason; phoneCallDiscardReasonHangup#57adc690 = PhoneCallDiscardReason; phoneCallDiscardReasonBusy#faf7e8c9 = PhoneCallDiscardReason; phoneCallDiscardReasonAllowGroupCall#afe2b839 encrypted_key:bytes = PhoneCallDiscardReason; dataJSON#7d748d04 data:string = DataJSON; labeledPrice#cb296bf8 label:string amount:long = LabeledPrice; invoice#49ee584 flags:# test:flags.0?true name_requested:flags.1?true phone_requested:flags.2?true email_requested:flags.3?true shipping_address_requested:flags.4?true flexible:flags.5?true phone_to_provider:flags.6?true email_to_provider:flags.7?true recurring:flags.9?true currency:string prices:Vector max_tip_amount:flags.8?long suggested_tip_amounts:flags.8?Vector terms_url:flags.10?string subscription_period:flags.11?int = Invoice; paymentCharge#ea02c27e id:string provider_charge_id:string = PaymentCharge; postAddress#1e8caaeb street_line1:string street_line2:string city:string state:string country_iso2:string post_code:string = PostAddress; paymentRequestedInfo#909c3f94 flags:# name:flags.0?string phone:flags.1?string email:flags.2?string shipping_address:flags.3?PostAddress = PaymentRequestedInfo; paymentSavedCredentialsCard#cdc27a1f id:string title:string = PaymentSavedCredentials; webDocument#1c570ed1 url:string access_hash:long size:int mime_type:string attributes:Vector = WebDocument; webDocumentNoProxy#f9c8bcc6 url:string size:int mime_type:string attributes:Vector = WebDocument; inputWebDocument#9bed434d url:string size:int mime_type:string attributes:Vector = InputWebDocument; inputWebFileLocation#c239d686 url:string access_hash:long = InputWebFileLocation; inputWebFileGeoPointLocation#9f2221c9 geo_point:InputGeoPoint access_hash:long w:int h:int zoom:int scale:int = InputWebFileLocation; inputWebFileAudioAlbumThumbLocation#f46fe924 flags:# small:flags.2?true document:flags.0?InputDocument title:flags.1?string performer:flags.1?string = InputWebFileLocation; upload.webFile#21e753bc size:int mime_type:string file_type:storage.FileType mtime:int bytes:bytes = upload.WebFile; payments.paymentForm#a0058751 flags:# can_save_credentials:flags.2?true password_missing:flags.3?true form_id:long bot_id:long title:string description:string photo:flags.5?WebDocument invoice:Invoice provider_id:long url:string native_provider:flags.4?string native_params:flags.4?DataJSON additional_methods:flags.6?Vector saved_info:flags.0?PaymentRequestedInfo saved_credentials:flags.1?Vector users:Vector = payments.PaymentForm; payments.paymentFormStars#7bf6b15c flags:# form_id:long bot_id:long title:string description:string photo:flags.5?WebDocument invoice:Invoice users:Vector = payments.PaymentForm; payments.paymentFormStarGift#b425cfe1 form_id:long invoice:Invoice = payments.PaymentForm; payments.validatedRequestedInfo#d1451883 flags:# id:flags.0?string shipping_options:flags.1?Vector = payments.ValidatedRequestedInfo; payments.paymentResult#4e5f810d updates:Updates = payments.PaymentResult; payments.paymentVerificationNeeded#d8411139 url:string = payments.PaymentResult; payments.paymentReceipt#70c4fe03 flags:# date:int bot_id:long provider_id:long title:string description:string photo:flags.2?WebDocument invoice:Invoice info:flags.0?PaymentRequestedInfo shipping:flags.1?ShippingOption tip_amount:flags.3?long currency:string total_amount:long credentials_title:string users:Vector = payments.PaymentReceipt; payments.paymentReceiptStars#dabbf83a flags:# date:int bot_id:long title:string description:string photo:flags.2?WebDocument invoice:Invoice currency:string total_amount:long transaction_id:string users:Vector = payments.PaymentReceipt; payments.savedInfo#fb8fe43c flags:# has_saved_credentials:flags.1?true saved_info:flags.0?PaymentRequestedInfo = payments.SavedInfo; inputPaymentCredentialsSaved#c10eb2cf id:string tmp_password:bytes = InputPaymentCredentials; inputPaymentCredentials#3417d728 flags:# save:flags.0?true data:DataJSON = InputPaymentCredentials; inputPaymentCredentialsApplePay#aa1c39f payment_data:DataJSON = InputPaymentCredentials; inputPaymentCredentialsGooglePay#8ac32801 payment_token:DataJSON = InputPaymentCredentials; account.tmpPassword#db64fd34 tmp_password:bytes valid_until:int = account.TmpPassword; shippingOption#b6213cdf id:string title:string prices:Vector = ShippingOption; inputStickerSetItem#32da9e9c flags:# document:InputDocument emoji:string mask_coords:flags.0?MaskCoords keywords:flags.1?string = InputStickerSetItem; inputPhoneCall#1e36fded id:long access_hash:long = InputPhoneCall; phoneCallEmpty#5366c915 id:long = PhoneCall; phoneCallWaiting#eed42858 flags:# video:flags.6?true id:long access_hash:long date:int admin_id:long participant_id:long protocol:PhoneCallProtocol receive_date:flags.0?int conference_call:flags.8?InputGroupCall = PhoneCall; phoneCallRequested#45361c63 flags:# video:flags.6?true id:long access_hash:long date:int admin_id:long participant_id:long g_a_hash:bytes protocol:PhoneCallProtocol conference_call:flags.8?InputGroupCall = PhoneCall; phoneCallAccepted#22fd7181 flags:# video:flags.6?true id:long access_hash:long date:int admin_id:long participant_id:long g_b:bytes protocol:PhoneCallProtocol conference_call:flags.8?InputGroupCall = PhoneCall; phoneCall#3ba5940c flags:# p2p_allowed:flags.5?true video:flags.6?true id:long access_hash:long date:int admin_id:long participant_id:long g_a_or_b:bytes key_fingerprint:long protocol:PhoneCallProtocol connections:Vector start_date:int custom_parameters:flags.7?DataJSON conference_call:flags.8?InputGroupCall = PhoneCall; phoneCallDiscarded#f9d25503 flags:# need_rating:flags.2?true need_debug:flags.3?true video:flags.6?true id:long reason:flags.0?PhoneCallDiscardReason duration:flags.1?int conference_call:flags.8?InputGroupCall = PhoneCall; phoneConnection#9cc123c7 flags:# tcp:flags.0?true id:long ip:string ipv6:string port:int peer_tag:bytes = PhoneConnection; phoneConnectionWebrtc#635fe375 flags:# turn:flags.0?true stun:flags.1?true id:long ip:string ipv6:string port:int username:string password:string = PhoneConnection; phoneCallProtocol#fc878fc8 flags:# udp_p2p:flags.0?true udp_reflector:flags.1?true min_layer:int max_layer:int library_versions:Vector = PhoneCallProtocol; phone.phoneCall#ec82e140 phone_call:PhoneCall users:Vector = phone.PhoneCall; upload.cdnFileReuploadNeeded#eea8e46e request_token:bytes = upload.CdnFile; upload.cdnFile#a99fca4f bytes:bytes = upload.CdnFile; cdnPublicKey#c982eaba dc_id:int public_key:string = CdnPublicKey; cdnConfig#5725e40a public_keys:Vector = CdnConfig; langPackString#cad181f6 key:string value:string = LangPackString; langPackStringPluralized#6c47ac9f flags:# key:string zero_value:flags.0?string one_value:flags.1?string two_value:flags.2?string few_value:flags.3?string many_value:flags.4?string other_value:string = LangPackString; langPackStringDeleted#2979eeb2 key:string = LangPackString; langPackDifference#f385c1f6 lang_code:string from_version:int version:int strings:Vector = LangPackDifference; langPackLanguage#eeca5ce3 flags:# official:flags.0?true rtl:flags.2?true beta:flags.3?true name:string native_name:string lang_code:string base_lang_code:flags.1?string plural_code:string strings_count:int translated_count:int translations_url:string = LangPackLanguage; channelAdminLogEventActionChangeTitle#e6dfb825 prev_value:string new_value:string = ChannelAdminLogEventAction; channelAdminLogEventActionChangeAbout#55188a2e prev_value:string new_value:string = ChannelAdminLogEventAction; channelAdminLogEventActionChangeUsername#6a4afc38 prev_value:string new_value:string = ChannelAdminLogEventAction; channelAdminLogEventActionChangePhoto#434bd2af prev_photo:Photo new_photo:Photo = ChannelAdminLogEventAction; channelAdminLogEventActionToggleInvites#1b7907ae new_value:Bool = ChannelAdminLogEventAction; channelAdminLogEventActionToggleSignatures#26ae0971 new_value:Bool = ChannelAdminLogEventAction; channelAdminLogEventActionUpdatePinned#e9e82c18 message:Message = ChannelAdminLogEventAction; channelAdminLogEventActionEditMessage#709b2405 prev_message:Message new_message:Message = ChannelAdminLogEventAction; channelAdminLogEventActionDeleteMessage#42e047bb message:Message = ChannelAdminLogEventAction; channelAdminLogEventActionParticipantJoin#183040d3 = ChannelAdminLogEventAction; channelAdminLogEventActionParticipantLeave#f89777f2 = ChannelAdminLogEventAction; channelAdminLogEventActionParticipantInvite#e31c34d8 participant:ChannelParticipant = ChannelAdminLogEventAction; 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; channelAdminLogEventActionDefaultBannedRights#2df5fc0a prev_banned_rights:ChatBannedRights new_banned_rights:ChatBannedRights = ChannelAdminLogEventAction; channelAdminLogEventActionStopPoll#8f079643 message:Message = ChannelAdminLogEventAction; channelAdminLogEventActionChangeLinkedChat#50c7ac8 prev_value:long new_value:long = ChannelAdminLogEventAction; channelAdminLogEventActionChangeLocation#e6b76ae prev_value:ChannelLocation new_value:ChannelLocation = ChannelAdminLogEventAction; channelAdminLogEventActionToggleSlowMode#53909779 prev_value:int new_value:int = ChannelAdminLogEventAction; channelAdminLogEventActionStartGroupCall#23209745 call:InputGroupCall = ChannelAdminLogEventAction; channelAdminLogEventActionDiscardGroupCall#db9f9140 call:InputGroupCall = ChannelAdminLogEventAction; channelAdminLogEventActionParticipantMute#f92424d2 participant:GroupCallParticipant = ChannelAdminLogEventAction; channelAdminLogEventActionParticipantUnmute#e64429c0 participant:GroupCallParticipant = ChannelAdminLogEventAction; channelAdminLogEventActionToggleGroupCallSetting#56d6a247 join_muted:Bool = ChannelAdminLogEventAction; channelAdminLogEventActionParticipantJoinByInvite#fe9fc158 flags:# via_chatlist:flags.0?true invite:ExportedChatInvite = ChannelAdminLogEventAction; channelAdminLogEventActionExportedInviteDelete#5a50fca4 invite:ExportedChatInvite = ChannelAdminLogEventAction; channelAdminLogEventActionExportedInviteRevoke#410a134e invite:ExportedChatInvite = ChannelAdminLogEventAction; channelAdminLogEventActionExportedInviteEdit#e90ebb59 prev_invite:ExportedChatInvite new_invite:ExportedChatInvite = ChannelAdminLogEventAction; channelAdminLogEventActionParticipantVolume#3e7f6847 participant:GroupCallParticipant = ChannelAdminLogEventAction; channelAdminLogEventActionChangeHistoryTTL#6e941a38 prev_value:int new_value:int = ChannelAdminLogEventAction; channelAdminLogEventActionParticipantJoinByRequest#afb6144a invite:ExportedChatInvite approved_by:long = ChannelAdminLogEventAction; channelAdminLogEventActionToggleNoForwards#cb2ac766 new_value:Bool = ChannelAdminLogEventAction; channelAdminLogEventActionSendMessage#278f2868 message:Message = ChannelAdminLogEventAction; channelAdminLogEventActionChangeAvailableReactions#be4e0ef8 prev_value:ChatReactions new_value:ChatReactions = ChannelAdminLogEventAction; channelAdminLogEventActionChangeUsernames#f04fb3a9 prev_value:Vector new_value:Vector = ChannelAdminLogEventAction; channelAdminLogEventActionToggleForum#2cc6383 new_value:Bool = ChannelAdminLogEventAction; channelAdminLogEventActionCreateTopic#58707d28 topic:ForumTopic = ChannelAdminLogEventAction; channelAdminLogEventActionEditTopic#f06fe208 prev_topic:ForumTopic new_topic:ForumTopic = ChannelAdminLogEventAction; channelAdminLogEventActionDeleteTopic#ae168909 topic:ForumTopic = ChannelAdminLogEventAction; channelAdminLogEventActionPinTopic#5d8d353b flags:# prev_topic:flags.0?ForumTopic new_topic:flags.1?ForumTopic = ChannelAdminLogEventAction; channelAdminLogEventActionToggleAntiSpam#64f36dfc new_value:Bool = ChannelAdminLogEventAction; channelAdminLogEventActionChangePeerColor#5796e780 prev_value:PeerColor new_value:PeerColor = ChannelAdminLogEventAction; channelAdminLogEventActionChangeProfilePeerColor#5e477b25 prev_value:PeerColor new_value:PeerColor = ChannelAdminLogEventAction; channelAdminLogEventActionChangeWallpaper#31bb5d52 prev_value:WallPaper new_value:WallPaper = ChannelAdminLogEventAction; channelAdminLogEventActionChangeEmojiStatus#3ea9feb1 prev_value:EmojiStatus new_value:EmojiStatus = ChannelAdminLogEventAction; channelAdminLogEventActionChangeEmojiStickerSet#46d840ab prev_stickerset:InputStickerSet new_stickerset:InputStickerSet = ChannelAdminLogEventAction; channelAdminLogEventActionToggleSignatureProfiles#60a79c79 new_value:Bool = ChannelAdminLogEventAction; channelAdminLogEventActionParticipantSubExtend#64642db3 prev_participant:ChannelParticipant new_participant:ChannelParticipant = ChannelAdminLogEventAction; channelAdminLogEvent#1fad68cd id:long date:int user_id:long action:ChannelAdminLogEventAction = ChannelAdminLogEvent; channels.adminLogResults#ed8af74d events:Vector chats:Vector users:Vector = channels.AdminLogResults; channelAdminLogEventsFilter#ea107ae4 flags:# join:flags.0?true leave:flags.1?true invite:flags.2?true ban:flags.3?true unban:flags.4?true kick:flags.5?true unkick:flags.6?true promote:flags.7?true demote:flags.8?true info:flags.9?true settings:flags.10?true pinned:flags.11?true edit:flags.12?true delete:flags.13?true group_call:flags.14?true invites:flags.15?true send:flags.16?true forums:flags.17?true sub_extend:flags.18?true = ChannelAdminLogEventsFilter; popularContact#5ce14175 client_id:long importers:int = PopularContact; messages.favedStickersNotModified#9e8fa6d3 = messages.FavedStickers; messages.favedStickers#2cb51097 hash:long packs:Vector stickers:Vector = messages.FavedStickers; recentMeUrlUnknown#46e1d13d url:string = RecentMeUrl; recentMeUrlUser#b92c09e2 url:string user_id:long = RecentMeUrl; recentMeUrlChat#b2da71d2 url:string chat_id:long = RecentMeUrl; recentMeUrlChatInvite#eb49081d url:string chat_invite:ChatInvite = RecentMeUrl; recentMeUrlStickerSet#bc0a57dc url:string set:StickerSetCovered = RecentMeUrl; help.recentMeUrls#e0310d7 urls:Vector chats:Vector users:Vector = help.RecentMeUrls; inputSingleMedia#1cc6e91f flags:# media:InputMedia random_id:long message:string entities:flags.0?Vector = InputSingleMedia; webAuthorization#a6f8f452 hash:long bot_id:long domain:string browser:string platform:string date_created:int date_active:int ip:string region:string = WebAuthorization; account.webAuthorizations#ed56c9fc authorizations:Vector users:Vector = account.WebAuthorizations; inputMessageID#a676a322 id:int = InputMessage; inputMessageReplyTo#bad88395 id:int = InputMessage; inputMessagePinned#86872538 = InputMessage; inputMessageCallbackQuery#acfa1a7e id:int query_id:long = InputMessage; inputDialogPeer#fcaafeb7 peer:InputPeer = InputDialogPeer; inputDialogPeerFolder#64600527 folder_id:int = InputDialogPeer; dialogPeer#e56dbf05 peer:Peer = DialogPeer; dialogPeerFolder#514519e2 folder_id:int = DialogPeer; messages.foundStickerSetsNotModified#d54b65d = messages.FoundStickerSets; messages.foundStickerSets#8af09dd2 hash:long sets:Vector = messages.FoundStickerSets; fileHash#f39b035c offset:long limit:int hash:bytes = FileHash; inputClientProxy#75588b3f address:string port:int = InputClientProxy; help.termsOfServiceUpdateEmpty#e3309f7f expires:int = help.TermsOfServiceUpdate; help.termsOfServiceUpdate#28ecf961 expires:int terms_of_service:help.TermsOfService = help.TermsOfServiceUpdate; inputSecureFileUploaded#3334b0f0 id:long parts:int md5_checksum:string file_hash:bytes secret:bytes = InputSecureFile; inputSecureFile#5367e5be id:long access_hash:long = InputSecureFile; secureFileEmpty#64199744 = SecureFile; secureFile#7d09c27e id:long access_hash:long size:long dc_id:int date:int file_hash:bytes secret:bytes = SecureFile; secureData#8aeabec3 data:bytes data_hash:bytes secret:bytes = SecureData; securePlainPhone#7d6099dd phone:string = SecurePlainData; securePlainEmail#21ec5a5f email:string = SecurePlainData; secureValueTypePersonalDetails#9d2a81e3 = SecureValueType; secureValueTypePassport#3dac6a00 = SecureValueType; secureValueTypeDriverLicense#6e425c4 = SecureValueType; secureValueTypeIdentityCard#a0d0744b = SecureValueType; secureValueTypeInternalPassport#99a48f23 = SecureValueType; secureValueTypeAddress#cbe31e26 = SecureValueType; secureValueTypeUtilityBill#fc36954e = SecureValueType; secureValueTypeBankStatement#89137c0d = SecureValueType; secureValueTypeRentalAgreement#8b883488 = SecureValueType; secureValueTypePassportRegistration#99e3806a = SecureValueType; secureValueTypeTemporaryRegistration#ea02ec33 = SecureValueType; secureValueTypePhone#b320aadb = SecureValueType; secureValueTypeEmail#8e3ca7ee = SecureValueType; secureValue#187fa0ca flags:# type:SecureValueType data:flags.0?SecureData front_side:flags.1?SecureFile reverse_side:flags.2?SecureFile selfie:flags.3?SecureFile translation:flags.6?Vector files:flags.4?Vector plain_data:flags.5?SecurePlainData hash:bytes = SecureValue; inputSecureValue#db21d0a7 flags:# type:SecureValueType data:flags.0?SecureData front_side:flags.1?InputSecureFile reverse_side:flags.2?InputSecureFile selfie:flags.3?InputSecureFile translation:flags.6?Vector files:flags.4?Vector plain_data:flags.5?SecurePlainData = InputSecureValue; secureValueHash#ed1ecdb0 type:SecureValueType hash:bytes = SecureValueHash; secureValueErrorData#e8a40bd9 type:SecureValueType data_hash:bytes field:string text:string = SecureValueError; secureValueErrorFrontSide#be3dfa type:SecureValueType file_hash:bytes text:string = SecureValueError; secureValueErrorReverseSide#868a2aa5 type:SecureValueType file_hash:bytes text:string = SecureValueError; secureValueErrorSelfie#e537ced6 type:SecureValueType file_hash:bytes text:string = SecureValueError; secureValueErrorFile#7a700873 type:SecureValueType file_hash:bytes text:string = SecureValueError; secureValueErrorFiles#666220e9 type:SecureValueType file_hash:Vector text:string = SecureValueError; secureValueError#869d758f type:SecureValueType hash:bytes text:string = SecureValueError; secureValueErrorTranslationFile#a1144770 type:SecureValueType file_hash:bytes text:string = SecureValueError; secureValueErrorTranslationFiles#34636dd8 type:SecureValueType file_hash:Vector text:string = SecureValueError; secureCredentialsEncrypted#33f0ea47 data:bytes hash:bytes secret:bytes = SecureCredentialsEncrypted; account.authorizationForm#ad2e1cd8 flags:# required_types:Vector values:Vector errors:Vector users:Vector privacy_policy_url:flags.0?string = account.AuthorizationForm; account.sentEmailCode#811f854f email_pattern:string length:int = account.SentEmailCode; help.deepLinkInfoEmpty#66afa166 = help.DeepLinkInfo; help.deepLinkInfo#6a4ee832 flags:# update_app:flags.0?true message:string entities:flags.1?Vector = help.DeepLinkInfo; savedPhoneContact#1142bd56 phone:string first_name:string last_name:string date:int = SavedContact; account.takeout#4dba4501 id:long = account.Takeout; passwordKdfAlgoUnknown#d45ab096 = PasswordKdfAlgo; passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow#3a912d4a salt1:bytes salt2:bytes g:int p:bytes = PasswordKdfAlgo; securePasswordKdfAlgoUnknown#4a8537 = SecurePasswordKdfAlgo; securePasswordKdfAlgoPBKDF2HMACSHA512iter100000#bbf2dda0 salt:bytes = SecurePasswordKdfAlgo; securePasswordKdfAlgoSHA512#86471d92 salt:bytes = SecurePasswordKdfAlgo; secureSecretSettings#1527bcac secure_algo:SecurePasswordKdfAlgo secure_secret:bytes secure_secret_id:long = SecureSecretSettings; inputCheckPasswordEmpty#9880f658 = InputCheckPasswordSRP; inputCheckPasswordSRP#d27ff082 srp_id:long A:bytes M1:bytes = InputCheckPasswordSRP; secureRequiredType#829d99da flags:# native_names:flags.0?true selfie_required:flags.1?true translation_required:flags.2?true type:SecureValueType = SecureRequiredType; secureRequiredTypeOneOf#27477b4 types:Vector = SecureRequiredType; help.passportConfigNotModified#bfb9f457 = help.PassportConfig; help.passportConfig#a098d6af hash:int countries_langs:DataJSON = help.PassportConfig; inputAppEvent#1d1b1245 time:double type:string peer:long data:JSONValue = InputAppEvent; jsonObjectValue#c0de1bd9 key:string value:JSONValue = JSONObjectValue; jsonNull#3f6d7b68 = JSONValue; jsonBool#c7345e6a value:Bool = JSONValue; jsonNumber#2be0dfa4 value:double = JSONValue; jsonString#b71e767a value:string = JSONValue; jsonArray#f7444763 value:Vector = JSONValue; jsonObject#99c1d49d value:Vector = JSONValue; pageTableCell#34566b6a flags:# header:flags.0?true align_center:flags.3?true align_right:flags.4?true valign_middle:flags.5?true valign_bottom:flags.6?true text:flags.7?RichText colspan:flags.1?int rowspan:flags.2?int = PageTableCell; pageTableRow#e0c0c5e5 cells:Vector = PageTableRow; pageCaption#6f747657 text:RichText credit:RichText = PageCaption; pageListItemText#b92fb6cd text:RichText = PageListItem; pageListItemBlocks#25e073fc blocks:Vector = PageListItem; pageListOrderedItemText#5e068047 num:string text:RichText = PageListOrderedItem; pageListOrderedItemBlocks#98dd8936 num:string blocks:Vector = PageListOrderedItem; pageRelatedArticle#b390dc08 flags:# url:string webpage_id:long title:flags.0?string description:flags.1?string photo_id:flags.2?long author:flags.3?string published_date:flags.4?int = PageRelatedArticle; page#98657f0d flags:# part:flags.0?true rtl:flags.1?true v2:flags.2?true url:string blocks:Vector photos:Vector documents:Vector views:flags.3?int = Page; help.supportName#8c05f1c9 name:string = help.SupportName; help.userInfoEmpty#f3ae2eed = help.UserInfo; help.userInfo#1eb3758 message:string entities:Vector author:string date:int = help.UserInfo; pollAnswer#ff16e2ca text:TextWithEntities option:bytes = PollAnswer; poll#58747131 id:long flags:# closed:flags.0?true public_voters:flags.1?true multiple_choice:flags.2?true quiz:flags.3?true question:TextWithEntities answers:Vector close_period:flags.4?int close_date:flags.5?int = Poll; pollAnswerVoters#3b6ddad2 flags:# chosen:flags.0?true correct:flags.1?true option:bytes voters:int = PollAnswerVoters; pollResults#7adf2420 flags:# min:flags.0?true results:flags.1?Vector total_voters:flags.2?int recent_voters:flags.3?Vector solution:flags.4?string solution_entities:flags.4?Vector = PollResults; chatOnlines#f041e250 onlines:int = ChatOnlines; statsURL#47a971e0 url:string = StatsURL; chatAdminRights#5fb224d5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true pin_messages:flags.7?true add_admins:flags.9?true anonymous:flags.10?true manage_call:flags.11?true other:flags.12?true manage_topics:flags.13?true post_stories:flags.14?true edit_stories:flags.15?true delete_stories:flags.16?true = ChatAdminRights; chatBannedRights#9f120418 flags:# view_messages:flags.0?true send_messages:flags.1?true send_media:flags.2?true send_stickers:flags.3?true send_gifs:flags.4?true send_games:flags.5?true send_inline:flags.6?true embed_links:flags.7?true send_polls:flags.8?true change_info:flags.10?true invite_users:flags.15?true pin_messages:flags.17?true manage_topics:flags.18?true send_photos:flags.19?true send_videos:flags.20?true send_roundvideos:flags.21?true send_audios:flags.22?true send_voices:flags.23?true send_docs:flags.24?true send_plain:flags.25?true until_date:int = ChatBannedRights; inputWallPaper#e630b979 id:long access_hash:long = InputWallPaper; inputWallPaperSlug#72091c80 slug:string = InputWallPaper; inputWallPaperNoFile#967a462e id:long = InputWallPaper; account.wallPapersNotModified#1c199183 = account.WallPapers; account.wallPapers#cdc3858c hash:long wallpapers:Vector = account.WallPapers; codeSettings#ad253d78 flags:# allow_flashcall:flags.0?true current_number:flags.1?true allow_app_hash:flags.4?true allow_missed_call:flags.5?true allow_firebase:flags.7?true unknown_number:flags.9?true logout_tokens:flags.6?Vector token:flags.8?string app_sandbox:flags.8?Bool = CodeSettings; wallPaperSettings#372efcd0 flags:# blur:flags.1?true motion:flags.2?true background_color:flags.0?int second_background_color:flags.4?int third_background_color:flags.5?int fourth_background_color:flags.6?int intensity:flags.3?int rotation:flags.4?int emoticon:flags.7?string = WallPaperSettings; autoDownloadSettings#baa57628 flags:# disabled:flags.0?true video_preload_large:flags.1?true audio_preload_next:flags.2?true phonecalls_less_data:flags.3?true stories_preload:flags.4?true photo_size_max:int video_size_max:long file_size_max:long video_upload_maxbitrate:int small_queue_active_operations_max:int large_queue_active_operations_max:int = AutoDownloadSettings; account.autoDownloadSettings#63cacf26 low:AutoDownloadSettings medium:AutoDownloadSettings high:AutoDownloadSettings = account.AutoDownloadSettings; emojiKeyword#d5b3b9f9 keyword:string emoticons:Vector = EmojiKeyword; emojiKeywordDeleted#236df622 keyword:string emoticons:Vector = EmojiKeyword; emojiKeywordsDifference#5cc761bd lang_code:string from_version:int version:int keywords:Vector = EmojiKeywordsDifference; emojiURL#a575739d url:string = EmojiURL; emojiLanguage#b3fb5361 lang_code:string = EmojiLanguage; folder#ff544e65 flags:# autofill_new_broadcasts:flags.0?true autofill_public_groups:flags.1?true autofill_new_correspondents:flags.2?true id:int title:string photo:flags.3?ChatPhoto = Folder; inputFolderPeer#fbd2c296 peer:InputPeer folder_id:int = InputFolderPeer; folderPeer#e9baa668 peer:Peer folder_id:int = FolderPeer; messages.searchCounter#e844ebff flags:# inexact:flags.1?true filter:MessagesFilter count:int = messages.SearchCounter; urlAuthResultRequest#92d33a0e flags:# request_write_access:flags.0?true bot:User domain:string = UrlAuthResult; urlAuthResultAccepted#8f8c0e4e url:string = UrlAuthResult; urlAuthResultDefault#a9d6db1f = UrlAuthResult; channelLocationEmpty#bfb5ad8b = ChannelLocation; channelLocation#209b82db geo_point:GeoPoint address:string = ChannelLocation; peerLocated#ca461b5d peer:Peer expires:int distance:int = PeerLocated; peerSelfLocated#f8ec284b expires:int = PeerLocated; restrictionReason#d072acb4 platform:string reason:string text:string = RestrictionReason; inputTheme#3c5693e9 id:long access_hash:long = InputTheme; inputThemeSlug#f5890df1 slug:string = InputTheme; theme#a00e67d6 flags:# creator:flags.0?true default:flags.1?true for_chat:flags.5?true id:long access_hash:long slug:string title:string document:flags.2?Document settings:flags.3?Vector emoticon:flags.6?string installs_count:flags.4?int = Theme; account.themesNotModified#f41eb622 = account.Themes; account.themes#9a3d8c6d hash:long themes:Vector = account.Themes; auth.loginToken#629f1980 expires:int token:bytes = auth.LoginToken; auth.loginTokenMigrateTo#68e9916 dc_id:int token:bytes = auth.LoginToken; auth.loginTokenSuccess#390d5c5e authorization:auth.Authorization = auth.LoginToken; account.contentSettings#57e28221 flags:# sensitive_enabled:flags.0?true sensitive_can_change:flags.1?true = account.ContentSettings; messages.inactiveChats#a927fec5 dates:Vector chats:Vector users:Vector = messages.InactiveChats; baseThemeClassic#c3a12462 = BaseTheme; baseThemeDay#fbd81688 = BaseTheme; baseThemeNight#b7b31ea8 = BaseTheme; baseThemeTinted#6d5f77ee = BaseTheme; baseThemeArctic#5b11125a = BaseTheme; inputThemeSettings#8fde504f flags:# message_colors_animated:flags.2?true base_theme:BaseTheme accent_color:int outbox_accent_color:flags.3?int message_colors:flags.0?Vector wallpaper:flags.1?InputWallPaper wallpaper_settings:flags.1?WallPaperSettings = InputThemeSettings; themeSettings#fa58b6d4 flags:# message_colors_animated:flags.2?true base_theme:BaseTheme accent_color:int outbox_accent_color:flags.3?int message_colors:flags.0?Vector wallpaper:flags.1?WallPaper = ThemeSettings; webPageAttributeTheme#54b56617 flags:# documents:flags.0?Vector settings:flags.1?ThemeSettings = WebPageAttribute; webPageAttributeStory#2e94c3e7 flags:# peer:Peer id:int story:flags.0?StoryItem = WebPageAttribute; webPageAttributeStickerSet#50cc03d3 flags:# emojis:flags.0?true text_color:flags.1?true stickers:Vector = WebPageAttribute; webPageAttributeUniqueStarGift#cf6f6db8 gift:StarGift = WebPageAttribute; messages.votesList#4899484e flags:# count:int votes:Vector chats:Vector users:Vector next_offset:flags.0?string = messages.VotesList; bankCardOpenUrl#f568028a url:string name:string = BankCardOpenUrl; payments.bankCardData#3e24e573 title:string open_urls:Vector = payments.BankCardData; dialogFilter#aa472651 flags:# contacts:flags.0?true non_contacts:flags.1?true groups:flags.2?true broadcasts:flags.3?true bots:flags.4?true exclude_muted:flags.11?true exclude_read:flags.12?true exclude_archived:flags.13?true title_noanimate:flags.28?true id:int title:TextWithEntities emoticon:flags.25?string color:flags.27?int pinned_peers:Vector include_peers:Vector exclude_peers:Vector = DialogFilter; dialogFilterDefault#363293ae = DialogFilter; dialogFilterChatlist#96537bd7 flags:# has_my_invites:flags.26?true title_noanimate:flags.28?true id:int title:TextWithEntities emoticon:flags.25?string color:flags.27?int pinned_peers:Vector include_peers:Vector = DialogFilter; dialogFilterSuggested#77744d4a filter:DialogFilter description:string = DialogFilterSuggested; statsDateRangeDays#b637edaf min_date:int max_date:int = StatsDateRangeDays; statsAbsValueAndPrev#cb43acde current:double previous:double = StatsAbsValueAndPrev; statsPercentValue#cbce2fe0 part:double total:double = StatsPercentValue; statsGraphAsync#4a27eb2d token:string = StatsGraph; statsGraphError#bedc9822 error:string = StatsGraph; statsGraph#8ea464b6 flags:# json:DataJSON zoom_token:flags.0?string = StatsGraph; stats.broadcastStats#396ca5fc period:StatsDateRangeDays followers:StatsAbsValueAndPrev views_per_post:StatsAbsValueAndPrev shares_per_post:StatsAbsValueAndPrev reactions_per_post:StatsAbsValueAndPrev views_per_story:StatsAbsValueAndPrev shares_per_story:StatsAbsValueAndPrev reactions_per_story:StatsAbsValueAndPrev enabled_notifications:StatsPercentValue growth_graph:StatsGraph followers_graph:StatsGraph mute_graph:StatsGraph top_hours_graph:StatsGraph interactions_graph:StatsGraph iv_interactions_graph:StatsGraph views_by_source_graph:StatsGraph new_followers_by_source_graph:StatsGraph languages_graph:StatsGraph reactions_by_emotion_graph:StatsGraph story_interactions_graph:StatsGraph story_reactions_by_emotion_graph:StatsGraph recent_posts_interactions:Vector = stats.BroadcastStats; help.promoDataEmpty#98f6ac75 expires:int = help.PromoData; help.promoData#8c39793f flags:# proxy:flags.0?true expires:int peer:Peer chats:Vector users:Vector psa_type:flags.1?string psa_message:flags.2?string = help.PromoData; videoSize#de33b094 flags:# type:string w:int h:int size:int video_start_ts:flags.0?double = VideoSize; videoSizeEmojiMarkup#f85c413c emoji_id:long background_colors:Vector = VideoSize; videoSizeStickerMarkup#da082fe stickerset:InputStickerSet sticker_id:long background_colors:Vector = VideoSize; statsGroupTopPoster#9d04af9b user_id:long messages:int avg_chars:int = StatsGroupTopPoster; statsGroupTopAdmin#d7584c87 user_id:long deleted:int kicked:int banned:int = StatsGroupTopAdmin; statsGroupTopInviter#535f779d user_id:long invitations:int = StatsGroupTopInviter; stats.megagroupStats#ef7ff916 period:StatsDateRangeDays members:StatsAbsValueAndPrev messages:StatsAbsValueAndPrev viewers:StatsAbsValueAndPrev posters:StatsAbsValueAndPrev growth_graph:StatsGraph members_graph:StatsGraph new_members_by_source_graph:StatsGraph languages_graph:StatsGraph messages_graph:StatsGraph actions_graph:StatsGraph top_hours_graph:StatsGraph weekdays_graph:StatsGraph top_posters:Vector top_admins:Vector top_inviters:Vector users:Vector = stats.MegagroupStats; globalPrivacySettings#734c4ccb flags:# archive_and_mute_new_noncontact_peers:flags.0?true keep_archived_unmuted:flags.1?true keep_archived_folders:flags.2?true hide_read_marks:flags.3?true new_noncontact_peers_require_premium:flags.4?true = GlobalPrivacySettings; help.countryCode#4203c5ef flags:# country_code:string prefixes:flags.0?Vector patterns:flags.1?Vector = help.CountryCode; help.country#c3878e23 flags:# hidden:flags.0?true iso2:string default_name:string name:flags.1?string country_codes:Vector = help.Country; help.countriesListNotModified#93cc1f32 = help.CountriesList; help.countriesList#87d0759e countries:Vector hash:int = help.CountriesList; messageViews#455b853d flags:# views:flags.0?int forwards:flags.1?int replies:flags.2?MessageReplies = MessageViews; messages.messageViews#b6c4f543 views:Vector chats:Vector users:Vector = messages.MessageViews; messages.discussionMessage#a6341782 flags:# messages:Vector max_id:flags.0?int read_inbox_max_id:flags.1?int read_outbox_max_id:flags.2?int unread_count:int chats:Vector users:Vector = messages.DiscussionMessage; messageReplyHeader#afbc09db flags:# reply_to_scheduled:flags.2?true forum_topic:flags.3?true quote:flags.9?true reply_to_msg_id:flags.4?int reply_to_peer_id:flags.0?Peer reply_from:flags.5?MessageFwdHeader reply_media:flags.8?MessageMedia reply_to_top_id:flags.1?int quote_text:flags.6?string quote_entities:flags.7?Vector quote_offset:flags.10?int = MessageReplyHeader; messageReplyStoryHeader#e5af939 peer:Peer story_id:int = MessageReplyHeader; messageReplies#83d60fc2 flags:# comments:flags.0?true replies:int replies_pts:int recent_repliers:flags.1?Vector channel_id:flags.0?long max_id:flags.2?int read_max_id:flags.3?int = MessageReplies; peerBlocked#e8fd8014 peer_id:Peer date:int = PeerBlocked; stats.messageStats#7fe91c14 views_graph:StatsGraph reactions_by_emotion_graph:StatsGraph = stats.MessageStats; groupCallDiscarded#7780bcb4 id:long access_hash:long duration:int = GroupCall; groupCall#cdf8d3e3 flags:# join_muted:flags.1?true can_change_join_muted:flags.2?true join_date_asc:flags.6?true schedule_start_subscribed:flags.8?true can_start_video:flags.9?true record_video_active:flags.11?true rtmp_stream:flags.12?true listeners_hidden:flags.13?true id:long access_hash:long participants_count:int title:flags.3?string stream_dc_id:flags.4?int record_start_date:flags.5?int schedule_date:flags.7?int unmuted_video_count:flags.10?int unmuted_video_limit:int version:int conference_from_call:flags.14?long = GroupCall; inputGroupCall#d8aa840f id:long access_hash:long = InputGroupCall; groupCallParticipant#eba636fe flags:# muted:flags.0?true left:flags.1?true can_self_unmute:flags.2?true just_joined:flags.4?true versioned:flags.5?true min:flags.8?true muted_by_you:flags.9?true volume_by_admin:flags.10?true self:flags.12?true video_joined:flags.15?true peer:Peer date:int active_date:flags.3?int source:int volume:flags.7?int about:flags.11?string raise_hand_rating:flags.13?long video:flags.6?GroupCallParticipantVideo presentation:flags.14?GroupCallParticipantVideo = GroupCallParticipant; phone.groupCall#9e727aad call:GroupCall participants:Vector participants_next_offset:string chats:Vector users:Vector = phone.GroupCall; phone.groupParticipants#f47751b6 count:int participants:Vector next_offset:string chats:Vector users:Vector version:int = phone.GroupParticipants; inlineQueryPeerTypeSameBotPM#3081ed9d = InlineQueryPeerType; inlineQueryPeerTypePM#833c0fac = InlineQueryPeerType; inlineQueryPeerTypeChat#d766c50a = InlineQueryPeerType; inlineQueryPeerTypeMegagroup#5ec4be43 = InlineQueryPeerType; inlineQueryPeerTypeBroadcast#6334ee9a = InlineQueryPeerType; inlineQueryPeerTypeBotPM#e3b2d0c = InlineQueryPeerType; messages.historyImport#1662af0b id:long = messages.HistoryImport; messages.historyImportParsed#5e0fb7b9 flags:# pm:flags.0?true group:flags.1?true title:flags.2?string = messages.HistoryImportParsed; messages.affectedFoundMessages#ef8d3e6c pts:int pts_count:int offset:int messages:Vector = messages.AffectedFoundMessages; chatInviteImporter#8c5adfd9 flags:# requested:flags.0?true via_chatlist:flags.3?true user_id:long date:int about:flags.2?string approved_by:flags.1?long = ChatInviteImporter; messages.exportedChatInvites#bdc62dcc count:int invites:Vector users:Vector = messages.ExportedChatInvites; messages.exportedChatInvite#1871be50 invite:ExportedChatInvite users:Vector = messages.ExportedChatInvite; messages.exportedChatInviteReplaced#222600ef invite:ExportedChatInvite new_invite:ExportedChatInvite users:Vector = messages.ExportedChatInvite; messages.chatInviteImporters#81b6b00a count:int importers:Vector users:Vector = messages.ChatInviteImporters; chatAdminWithInvites#f2ecef23 admin_id:long invites_count:int revoked_invites_count:int = ChatAdminWithInvites; messages.chatAdminsWithInvites#b69b72d7 admins:Vector users:Vector = messages.ChatAdminsWithInvites; messages.checkedHistoryImportPeer#a24de717 confirm_text:string = messages.CheckedHistoryImportPeer; phone.joinAsPeers#afe5623f peers:Vector chats:Vector users:Vector = phone.JoinAsPeers; phone.exportedGroupCallInvite#204bd158 link:string = phone.ExportedGroupCallInvite; groupCallParticipantVideoSourceGroup#dcb118b7 semantics:string sources:Vector = GroupCallParticipantVideoSourceGroup; groupCallParticipantVideo#67753ac8 flags:# paused:flags.0?true endpoint:string source_groups:Vector audio_source:flags.1?int = GroupCallParticipantVideo; stickers.suggestedShortName#85fea03f short_name:string = stickers.SuggestedShortName; botCommandScopeDefault#2f6cb2ab = BotCommandScope; botCommandScopeUsers#3c4f04d8 = BotCommandScope; botCommandScopeChats#6fe1a881 = BotCommandScope; botCommandScopeChatAdmins#b9aa606a = BotCommandScope; botCommandScopePeer#db9d897d peer:InputPeer = BotCommandScope; botCommandScopePeerAdmins#3fd863d1 peer:InputPeer = BotCommandScope; botCommandScopePeerUser#a1321f3 peer:InputPeer user_id:InputUser = BotCommandScope; account.resetPasswordFailedWait#e3779861 retry_date:int = account.ResetPasswordResult; account.resetPasswordRequestedWait#e9effc7d until_date:int = account.ResetPasswordResult; account.resetPasswordOk#e926d63e = account.ResetPasswordResult; sponsoredMessage#4d93a990 flags:# recommended:flags.5?true can_report:flags.12?true random_id:bytes url:string title:string message:string entities:flags.1?Vector photo:flags.6?Photo media:flags.14?MessageMedia color:flags.13?PeerColor button_text:string sponsor_info:flags.7?string additional_info:flags.8?string = SponsoredMessage; messages.sponsoredMessages#c9ee1d87 flags:# posts_between:flags.0?int messages:Vector chats:Vector users:Vector = messages.SponsoredMessages; messages.sponsoredMessagesEmpty#1839490f = messages.SponsoredMessages; searchResultsCalendarPeriod#c9b0539f date:int min_msg_id:int max_msg_id:int count:int = SearchResultsCalendarPeriod; messages.searchResultsCalendar#147ee23c flags:# inexact:flags.0?true count:int min_date:int min_msg_id:int offset_id_offset:flags.1?int periods:Vector messages:Vector chats:Vector users:Vector = messages.SearchResultsCalendar; searchResultPosition#7f648b67 msg_id:int date:int offset:int = SearchResultsPosition; messages.searchResultsPositions#53b22baf count:int positions:Vector = messages.SearchResultsPositions; channels.sendAsPeers#f496b0c6 peers:Vector chats:Vector users:Vector = channels.SendAsPeers; users.userFull#3b6d152e full_user:UserFull chats:Vector users:Vector = users.UserFull; messages.peerSettings#6880b94d settings:PeerSettings chats:Vector users:Vector = messages.PeerSettings; auth.loggedOut#c3a2835f flags:# future_auth_token:flags.0?bytes = auth.LoggedOut; reactionCount#a3d1cb80 flags:# chosen_order:flags.0?int reaction:Reaction count:int = ReactionCount; messageReactions#a339f0b flags:# min:flags.0?true can_see_list:flags.2?true reactions_as_tags:flags.3?true results:Vector recent_reactions:flags.1?Vector top_reactors:flags.4?Vector = MessageReactions; messages.messageReactionsList#31bd492d flags:# count:int reactions:Vector chats:Vector users:Vector next_offset:flags.0?string = messages.MessageReactionsList; availableReaction#c077ec01 flags:# inactive:flags.0?true premium:flags.2?true reaction:string title:string static_icon:Document appear_animation:Document select_animation:Document activate_animation:Document effect_animation:Document around_animation:flags.1?Document center_icon:flags.1?Document = AvailableReaction; messages.availableReactionsNotModified#9f071957 = messages.AvailableReactions; messages.availableReactions#768e3aad hash:int reactions:Vector = messages.AvailableReactions; messagePeerReaction#8c79b63c flags:# big:flags.0?true unread:flags.1?true my:flags.2?true peer_id:Peer date:int reaction:Reaction = MessagePeerReaction; groupCallStreamChannel#80eb48af channel:int scale:int last_timestamp_ms:long = GroupCallStreamChannel; phone.groupCallStreamChannels#d0e482b2 channels:Vector = phone.GroupCallStreamChannels; phone.groupCallStreamRtmpUrl#2dbf3432 url:string key:string = phone.GroupCallStreamRtmpUrl; attachMenuBotIconColor#4576f3f0 name:string color:int = AttachMenuBotIconColor; attachMenuBotIcon#b2a7386b flags:# name:string icon:Document colors:flags.0?Vector = AttachMenuBotIcon; attachMenuBot#d90d8dfe flags:# inactive:flags.0?true has_settings:flags.1?true request_write_access:flags.2?true show_in_attach_menu:flags.3?true show_in_side_menu:flags.4?true side_menu_disclaimer_needed:flags.5?true bot_id:long short_name:string peer_types:flags.3?Vector icons:Vector = AttachMenuBot; attachMenuBotsNotModified#f1d88a5c = AttachMenuBots; attachMenuBots#3c4301c0 hash:long bots:Vector users:Vector = AttachMenuBots; attachMenuBotsBot#93bf667f bot:AttachMenuBot users:Vector = AttachMenuBotsBot; webViewResultUrl#4d22ff98 flags:# fullsize:flags.1?true fullscreen:flags.2?true query_id:flags.0?long url:string = WebViewResult; webViewMessageSent#c94511c flags:# msg_id:flags.0?InputBotInlineMessageID = WebViewMessageSent; botMenuButtonDefault#7533a588 = BotMenuButton; botMenuButtonCommands#4258c205 = BotMenuButton; botMenuButton#c7b57ce6 text:string url:string = BotMenuButton; account.savedRingtonesNotModified#fbf6e8b1 = account.SavedRingtones; account.savedRingtones#c1e92cc5 hash:long ringtones:Vector = account.SavedRingtones; notificationSoundDefault#97e8bebe = NotificationSound; notificationSoundNone#6f0c34df = NotificationSound; notificationSoundLocal#830b9ae4 title:string data:string = NotificationSound; notificationSoundRingtone#ff6c8049 id:long = NotificationSound; account.savedRingtone#b7263f6d = account.SavedRingtone; account.savedRingtoneConverted#1f307eb7 document:Document = account.SavedRingtone; attachMenuPeerTypeSameBotPM#7d6be90e = AttachMenuPeerType; attachMenuPeerTypeBotPM#c32bfa1a = AttachMenuPeerType; attachMenuPeerTypePM#f146d31f = AttachMenuPeerType; attachMenuPeerTypeChat#509113f = AttachMenuPeerType; attachMenuPeerTypeBroadcast#7bfbdefc = AttachMenuPeerType; inputInvoiceMessage#c5b56859 peer:InputPeer msg_id:int = InputInvoice; inputInvoiceSlug#c326caef slug:string = InputInvoice; inputInvoicePremiumGiftCode#98986c0d purpose:InputStorePaymentPurpose option:PremiumGiftCodeOption = InputInvoice; inputInvoiceStars#65f00ce3 purpose:InputStorePaymentPurpose = InputInvoice; inputInvoiceChatInviteSubscription#34e793f1 hash:string = InputInvoice; inputInvoiceStarGift#e8625e92 flags:# hide_name:flags.0?true include_upgrade:flags.2?true peer:InputPeer gift_id:long message:flags.1?TextWithEntities = InputInvoice; inputInvoiceStarGiftUpgrade#4d818d5d flags:# keep_original_details:flags.0?true stargift:InputSavedStarGift = InputInvoice; inputInvoiceStarGiftTransfer#4a5f5bd9 stargift:InputSavedStarGift to_id:InputPeer = InputInvoice; payments.exportedInvoice#aed0cbd9 url:string = payments.ExportedInvoice; messages.transcribedAudio#cfb9d957 flags:# pending:flags.0?true transcription_id:long text:string trial_remains_num:flags.1?int trial_remains_until_date:flags.1?int = messages.TranscribedAudio; help.premiumPromo#5334759c status_text:string status_entities:Vector video_sections:Vector videos:Vector period_options:Vector users:Vector = help.PremiumPromo; inputStorePaymentPremiumSubscription#a6751e66 flags:# restore:flags.0?true upgrade:flags.1?true = InputStorePaymentPurpose; inputStorePaymentGiftPremium#616f7fe8 user_id:InputUser currency:string amount:long = InputStorePaymentPurpose; inputStorePaymentPremiumGiftCode#fb790393 flags:# users:Vector boost_peer:flags.0?InputPeer currency:string amount:long message:flags.1?TextWithEntities = InputStorePaymentPurpose; inputStorePaymentPremiumGiveaway#160544ca flags:# only_new_subscribers:flags.0?true winners_are_visible:flags.3?true boost_peer:InputPeer additional_peers:flags.1?Vector countries_iso2:flags.2?Vector prize_description:flags.4?string random_id:long until_date:int currency:string amount:long = InputStorePaymentPurpose; inputStorePaymentStarsTopup#dddd0f56 stars:long currency:string amount:long = InputStorePaymentPurpose; inputStorePaymentStarsGift#1d741ef7 user_id:InputUser stars:long currency:string amount:long = InputStorePaymentPurpose; inputStorePaymentStarsGiveaway#751f08fa flags:# only_new_subscribers:flags.0?true winners_are_visible:flags.3?true stars:long boost_peer:InputPeer additional_peers:flags.1?Vector countries_iso2:flags.2?Vector prize_description:flags.4?string random_id:long until_date:int currency:string amount:long users:int = InputStorePaymentPurpose; premiumGiftOption#74c34319 flags:# months:int currency:string amount:long bot_url:string store_product:flags.0?string = PremiumGiftOption; paymentFormMethod#88f8f21b url:string title:string = PaymentFormMethod; emojiStatusEmpty#2de11aae = EmojiStatus; emojiStatus#e7ff068a flags:# document_id:long until:flags.0?int = EmojiStatus; emojiStatusCollectible#7184603b flags:# collectible_id:long document_id:long title:string slug:string pattern_document_id:long center_color:int edge_color:int pattern_color:int text_color:int until:flags.0?int = EmojiStatus; inputEmojiStatusCollectible#7141dbf flags:# collectible_id:long until:flags.0?int = EmojiStatus; account.emojiStatusesNotModified#d08ce645 = account.EmojiStatuses; account.emojiStatuses#90c467d1 hash:long statuses:Vector = account.EmojiStatuses; reactionEmpty#79f5d419 = Reaction; reactionEmoji#1b2286b8 emoticon:string = Reaction; reactionCustomEmoji#8935fc73 document_id:long = Reaction; reactionPaid#523da4eb = Reaction; chatReactionsNone#eafc32bc = ChatReactions; chatReactionsAll#52928bca flags:# allow_custom:flags.0?true = ChatReactions; chatReactionsSome#661d4037 reactions:Vector = ChatReactions; messages.reactionsNotModified#b06fdbdf = messages.Reactions; messages.reactions#eafdf716 hash:long reactions:Vector = messages.Reactions; emailVerifyPurposeLoginSetup#4345be73 phone_number:string phone_code_hash:string = EmailVerifyPurpose; emailVerifyPurposeLoginChange#527d22eb = EmailVerifyPurpose; emailVerifyPurposePassport#bbf51685 = EmailVerifyPurpose; emailVerificationCode#922e55a9 code:string = EmailVerification; emailVerificationGoogle#db909ec2 token:string = EmailVerification; emailVerificationApple#96d074fd token:string = EmailVerification; account.emailVerified#2b96cd1b email:string = account.EmailVerified; account.emailVerifiedLogin#e1bb0d61 email:string sent_code:auth.SentCode = account.EmailVerified; premiumSubscriptionOption#5f2d1df2 flags:# current:flags.1?true can_purchase_upgrade:flags.2?true transaction:flags.3?string months:int currency:string amount:long bot_url:string store_product:flags.0?string = PremiumSubscriptionOption; sendAsPeer#b81c7034 flags:# premium_required:flags.0?true peer:Peer = SendAsPeer; messageExtendedMediaPreview#ad628cc8 flags:# w:flags.0?int h:flags.0?int thumb:flags.1?PhotoSize video_duration:flags.2?int = MessageExtendedMedia; messageExtendedMedia#ee479c64 media:MessageMedia = MessageExtendedMedia; stickerKeyword#fcfeb29c document_id:long keyword:Vector = StickerKeyword; username#b4073647 flags:# editable:flags.0?true active:flags.1?true username:string = Username; forumTopicDeleted#23f109b id:int = ForumTopic; forumTopic#71701da9 flags:# my:flags.1?true closed:flags.2?true pinned:flags.3?true short:flags.5?true hidden:flags.6?true id:int date:int title:string icon_color:int icon_emoji_id:flags.0?long top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int unread_reactions_count:int from_id:Peer notify_settings:PeerNotifySettings draft:flags.4?DraftMessage = ForumTopic; messages.forumTopics#367617d3 flags:# order_by_create_date:flags.0?true count:int topics:Vector messages:Vector chats:Vector users:Vector pts:int = messages.ForumTopics; defaultHistoryTTL#43b46b20 period:int = DefaultHistoryTTL; exportedContactToken#41bf109b url:string expires:int = ExportedContactToken; requestPeerTypeUser#5f3b8a00 flags:# bot:flags.0?Bool premium:flags.1?Bool = RequestPeerType; requestPeerTypeChat#c9f06e1b flags:# creator:flags.0?true bot_participant:flags.5?true has_username:flags.3?Bool forum:flags.4?Bool user_admin_rights:flags.1?ChatAdminRights bot_admin_rights:flags.2?ChatAdminRights = RequestPeerType; requestPeerTypeBroadcast#339bef6c flags:# creator:flags.0?true has_username:flags.3?Bool user_admin_rights:flags.1?ChatAdminRights bot_admin_rights:flags.2?ChatAdminRights = RequestPeerType; emojiListNotModified#481eadfa = EmojiList; emojiList#7a1e11d1 hash:long document_id:Vector = EmojiList; emojiGroup#7a9abda9 title:string icon_emoji_id:long emoticons:Vector = EmojiGroup; emojiGroupGreeting#80d26cc7 title:string icon_emoji_id:long emoticons:Vector = EmojiGroup; emojiGroupPremium#93bcf34 title:string icon_emoji_id:long = EmojiGroup; messages.emojiGroupsNotModified#6fb4ad87 = messages.EmojiGroups; messages.emojiGroups#881fb94b hash:int groups:Vector = messages.EmojiGroups; textWithEntities#751f3146 text:string entities:Vector = TextWithEntities; messages.translateResult#33db32f8 result:Vector = messages.TranslatedText; autoSaveSettings#c84834ce flags:# photos:flags.0?true videos:flags.1?true video_max_size:flags.2?long = AutoSaveSettings; autoSaveException#81602d47 peer:Peer settings:AutoSaveSettings = AutoSaveException; account.autoSaveSettings#4c3e069d users_settings:AutoSaveSettings chats_settings:AutoSaveSettings broadcasts_settings:AutoSaveSettings exceptions:Vector chats:Vector users:Vector = account.AutoSaveSettings; help.appConfigNotModified#7cde641d = help.AppConfig; help.appConfig#dd18782e hash:int config:JSONValue = help.AppConfig; inputBotAppID#a920bd7a id:long access_hash:long = InputBotApp; inputBotAppShortName#908c0407 bot_id:InputUser short_name:string = InputBotApp; botAppNotModified#5da674b7 = BotApp; botApp#95fcd1d6 flags:# id:long access_hash:long short_name:string title:string description:string photo:Photo document:flags.0?Document hash:long = BotApp; messages.botApp#eb50adf5 flags:# inactive:flags.0?true request_write_access:flags.1?true has_settings:flags.2?true app:BotApp = messages.BotApp; inlineBotWebView#b57295d5 text:string url:string = InlineBotWebView; readParticipantDate#4a4ff172 user_id:long date:int = ReadParticipantDate; inputChatlistDialogFilter#f3e0da33 filter_id:int = InputChatlist; exportedChatlistInvite#c5181ac flags:# title:string url:string peers:Vector = ExportedChatlistInvite; chatlists.exportedChatlistInvite#10e6e3a6 filter:DialogFilter invite:ExportedChatlistInvite = chatlists.ExportedChatlistInvite; chatlists.exportedInvites#10ab6dc7 invites:Vector chats:Vector users:Vector = chatlists.ExportedInvites; chatlists.chatlistInviteAlready#fa87f659 filter_id:int missing_peers:Vector already_peers:Vector chats:Vector users:Vector = chatlists.ChatlistInvite; chatlists.chatlistInvite#f10ece2f flags:# title_noanimate:flags.1?true title:TextWithEntities emoticon:flags.0?string peers:Vector chats:Vector users:Vector = chatlists.ChatlistInvite; chatlists.chatlistUpdates#93bd878d missing_peers:Vector chats:Vector users:Vector = chatlists.ChatlistUpdates; bots.botInfo#e8a775b0 name:string about:string description:string = bots.BotInfo; messagePeerVote#b6cc2d5c peer:Peer option:bytes date:int = MessagePeerVote; messagePeerVoteInputOption#74cda504 peer:Peer date:int = MessagePeerVote; messagePeerVoteMultiple#4628f6e6 peer:Peer options:Vector date:int = MessagePeerVote; storyViews#8d595cd6 flags:# has_viewers:flags.1?true views_count:int forwards_count:flags.2?int reactions:flags.3?Vector reactions_count:flags.4?int recent_viewers:flags.0?Vector = StoryViews; storyItemDeleted#51e6ee4f id:int = StoryItem; storyItemSkipped#ffadc913 flags:# close_friends:flags.8?true id:int date:int expire_date:int = StoryItem; storyItem#79b26a24 flags:# pinned:flags.5?true public:flags.7?true close_friends:flags.8?true min:flags.9?true noforwards:flags.10?true edited:flags.11?true contacts:flags.12?true selected_contacts:flags.13?true out:flags.16?true id:int date:int from_id:flags.18?Peer fwd_from:flags.17?StoryFwdHeader expire_date:int caption:flags.0?string entities:flags.1?Vector media:MessageMedia media_areas:flags.14?Vector privacy:flags.2?Vector views:flags.3?StoryViews sent_reaction:flags.15?Reaction = StoryItem; stories.allStoriesNotModified#1158fe3e flags:# state:string stealth_mode:StoriesStealthMode = stories.AllStories; stories.allStories#6efc5e81 flags:# has_more:flags.0?true count:int state:string peer_stories:Vector chats:Vector users:Vector stealth_mode:StoriesStealthMode = stories.AllStories; stories.stories#63c3dd0a flags:# count:int stories:Vector pinned_to_top:flags.0?Vector chats:Vector users:Vector = stories.Stories; storyView#b0bdeac5 flags:# blocked:flags.0?true blocked_my_stories_from:flags.1?true user_id:long date:int reaction:flags.2?Reaction = StoryView; storyViewPublicForward#9083670b flags:# blocked:flags.0?true blocked_my_stories_from:flags.1?true message:Message = StoryView; storyViewPublicRepost#bd74cf49 flags:# blocked:flags.0?true blocked_my_stories_from:flags.1?true peer_id:Peer story:StoryItem = StoryView; stories.storyViewsList#59d78fc5 flags:# count:int views_count:int forwards_count:int reactions_count:int views:Vector chats:Vector users:Vector next_offset:flags.0?string = stories.StoryViewsList; stories.storyViews#de9eed1d views:Vector users:Vector = stories.StoryViews; inputReplyToMessage#22c0f6d5 flags:# reply_to_msg_id:int top_msg_id:flags.0?int reply_to_peer_id:flags.1?InputPeer quote_text:flags.2?string quote_entities:flags.3?Vector quote_offset:flags.4?int = InputReplyTo; inputReplyToStory#5881323a peer:InputPeer story_id:int = InputReplyTo; exportedStoryLink#3fc9053b link:string = ExportedStoryLink; storiesStealthMode#712e27fd flags:# active_until_date:flags.0?int cooldown_until_date:flags.1?int = StoriesStealthMode; mediaAreaCoordinates#cfc9e002 flags:# x:double y:double w:double h:double rotation:double radius:flags.0?double = MediaAreaCoordinates; mediaAreaVenue#be82db9c coordinates:MediaAreaCoordinates geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string = MediaArea; inputMediaAreaVenue#b282217f coordinates:MediaAreaCoordinates query_id:long result_id:string = MediaArea; mediaAreaGeoPoint#cad5452d flags:# coordinates:MediaAreaCoordinates geo:GeoPoint address:flags.0?GeoPointAddress = MediaArea; mediaAreaSuggestedReaction#14455871 flags:# dark:flags.0?true flipped:flags.1?true coordinates:MediaAreaCoordinates reaction:Reaction = MediaArea; mediaAreaChannelPost#770416af coordinates:MediaAreaCoordinates channel_id:long msg_id:int = MediaArea; inputMediaAreaChannelPost#2271f2bf coordinates:MediaAreaCoordinates channel:InputChannel msg_id:int = MediaArea; mediaAreaUrl#37381085 coordinates:MediaAreaCoordinates url:string = MediaArea; mediaAreaWeather#49a6549c coordinates:MediaAreaCoordinates emoji:string temperature_c:double color:int = MediaArea; mediaAreaStarGift#5787686d coordinates:MediaAreaCoordinates slug:string = MediaArea; peerStories#9a35e999 flags:# peer:Peer max_read_id:flags.0?int stories:Vector = PeerStories; stories.peerStories#cae68768 stories:PeerStories chats:Vector users:Vector = stories.PeerStories; messages.webPage#fd5e12bd webpage:WebPage chats:Vector users:Vector = messages.WebPage; premiumGiftCodeOption#257e962b flags:# users:int months:int store_product:flags.0?string store_quantity:flags.1?int currency:string amount:long = PremiumGiftCodeOption; payments.checkedGiftCode#284a1096 flags:# via_giveaway:flags.2?true from_id:flags.4?Peer giveaway_msg_id:flags.3?int to_id:flags.0?long date:int months:int used_date:flags.1?int chats:Vector users:Vector = payments.CheckedGiftCode; payments.giveawayInfo#4367daa0 flags:# participating:flags.0?true preparing_results:flags.3?true start_date:int joined_too_early_date:flags.1?int admin_disallowed_chat_id:flags.2?long disallowed_country:flags.4?string = payments.GiveawayInfo; payments.giveawayInfoResults#e175e66f flags:# winner:flags.0?true refunded:flags.1?true start_date:int gift_code_slug:flags.3?string stars_prize:flags.4?long finish_date:int winners_count:int activated_count:flags.2?int = payments.GiveawayInfo; prepaidGiveaway#b2539d54 id:long months:int quantity:int date:int = PrepaidGiveaway; prepaidStarsGiveaway#9a9d77e0 id:long stars:long quantity:int boosts:int date:int = PrepaidGiveaway; boost#4b3e14d6 flags:# gift:flags.1?true giveaway:flags.2?true unclaimed:flags.3?true id:string user_id:flags.0?long giveaway_msg_id:flags.2?int date:int expires:int used_gift_slug:flags.4?string multiplier:flags.5?int stars:flags.6?long = Boost; premium.boostsList#86f8613c flags:# count:int boosts:Vector next_offset:flags.0?string users:Vector = premium.BoostsList; myBoost#c448415c flags:# slot:int peer:flags.0?Peer date:int expires:int cooldown_until_date:flags.1?int = MyBoost; premium.myBoosts#9ae228e2 my_boosts:Vector chats:Vector users:Vector = premium.MyBoosts; premium.boostsStatus#4959427a flags:# my_boost:flags.2?true level:int current_level_boosts:int boosts:int gift_boosts:flags.4?int next_level_boosts:flags.0?int premium_audience:flags.1?StatsPercentValue boost_url:string prepaid_giveaways:flags.3?Vector my_boost_slots:flags.2?Vector = premium.BoostsStatus; storyFwdHeader#b826e150 flags:# modified:flags.3?true from:flags.0?Peer from_name:flags.1?string story_id:flags.2?int = StoryFwdHeader; postInteractionCountersMessage#e7058e7f msg_id:int views:int forwards:int reactions:int = PostInteractionCounters; postInteractionCountersStory#8a480e27 story_id:int views:int forwards:int reactions:int = PostInteractionCounters; stats.storyStats#50cd067c views_graph:StatsGraph reactions_by_emotion_graph:StatsGraph = stats.StoryStats; publicForwardMessage#1f2bf4a message:Message = PublicForward; publicForwardStory#edf3add0 peer:Peer story:StoryItem = PublicForward; stats.publicForwards#93037e20 flags:# count:int forwards:Vector next_offset:flags.0?string chats:Vector users:Vector = stats.PublicForwards; peerColor#b54b5acf flags:# color:flags.0?int background_emoji_id:flags.1?long = PeerColor; help.peerColorSet#26219a58 colors:Vector = help.PeerColorSet; help.peerColorProfileSet#767d61eb palette_colors:Vector bg_colors:Vector story_colors:Vector = help.PeerColorSet; help.peerColorOption#adec6ebe flags:# hidden:flags.0?true color_id:int colors:flags.1?help.PeerColorSet dark_colors:flags.2?help.PeerColorSet channel_min_level:flags.3?int group_min_level:flags.4?int = help.PeerColorOption; help.peerColorsNotModified#2ba1f5ce = help.PeerColors; help.peerColors#f8ed08 hash:int colors:Vector = help.PeerColors; storyReaction#6090d6d5 peer_id:Peer date:int reaction:Reaction = StoryReaction; storyReactionPublicForward#bbab2643 message:Message = StoryReaction; storyReactionPublicRepost#cfcd0f13 peer_id:Peer story:StoryItem = StoryReaction; stories.storyReactionsList#aa5f789c flags:# count:int reactions:Vector chats:Vector users:Vector next_offset:flags.0?string = stories.StoryReactionsList; savedDialog#bd87cb6c flags:# pinned:flags.2?true peer:Peer top_message:int = SavedDialog; messages.savedDialogs#f83ae221 dialogs:Vector messages:Vector chats:Vector users:Vector = messages.SavedDialogs; messages.savedDialogsSlice#44ba9dd9 count:int dialogs:Vector messages:Vector chats:Vector users:Vector = messages.SavedDialogs; messages.savedDialogsNotModified#c01f6fe8 count:int = messages.SavedDialogs; savedReactionTag#cb6ff828 flags:# reaction:Reaction title:flags.0?string count:int = SavedReactionTag; messages.savedReactionTagsNotModified#889b59ef = messages.SavedReactionTags; messages.savedReactionTags#3259950a tags:Vector hash:long = messages.SavedReactionTags; outboxReadDate#3bb842ac date:int = OutboxReadDate; smsjobs.eligibleToJoin#dc8b44cf terms_url:string monthly_sent_sms:int = smsjobs.EligibilityToJoin; smsjobs.status#2aee9191 flags:# allow_international:flags.0?true recent_sent:int recent_since:int recent_remains:int total_sent:int total_since:int last_gift_slug:flags.1?string terms_url:string = smsjobs.Status; smsJob#e6a1eeb8 job_id:string phone_number:string text:string = SmsJob; businessWeeklyOpen#120b1ab9 start_minute:int end_minute:int = BusinessWeeklyOpen; businessWorkHours#8c92b098 flags:# open_now:flags.0?true timezone_id:string weekly_open:Vector = BusinessWorkHours; businessLocation#ac5c1af7 flags:# geo_point:flags.0?GeoPoint address:string = BusinessLocation; inputBusinessRecipients#6f8b32aa flags:# existing_chats:flags.0?true new_chats:flags.1?true contacts:flags.2?true non_contacts:flags.3?true exclude_selected:flags.5?true users:flags.4?Vector = InputBusinessRecipients; businessRecipients#21108ff7 flags:# existing_chats:flags.0?true new_chats:flags.1?true contacts:flags.2?true non_contacts:flags.3?true exclude_selected:flags.5?true users:flags.4?Vector = BusinessRecipients; businessAwayMessageScheduleAlways#c9b9e2b9 = BusinessAwayMessageSchedule; businessAwayMessageScheduleOutsideWorkHours#c3f2f501 = BusinessAwayMessageSchedule; businessAwayMessageScheduleCustom#cc4d9ecc start_date:int end_date:int = BusinessAwayMessageSchedule; inputBusinessGreetingMessage#194cb3b shortcut_id:int recipients:InputBusinessRecipients no_activity_days:int = InputBusinessGreetingMessage; businessGreetingMessage#e519abab shortcut_id:int recipients:BusinessRecipients no_activity_days:int = BusinessGreetingMessage; inputBusinessAwayMessage#832175e0 flags:# offline_only:flags.0?true shortcut_id:int schedule:BusinessAwayMessageSchedule recipients:InputBusinessRecipients = InputBusinessAwayMessage; businessAwayMessage#ef156a5c flags:# offline_only:flags.0?true shortcut_id:int schedule:BusinessAwayMessageSchedule recipients:BusinessRecipients = BusinessAwayMessage; timezone#ff9289f5 id:string name:string utc_offset:int = Timezone; help.timezonesListNotModified#970708cc = help.TimezonesList; help.timezonesList#7b74ed71 timezones:Vector hash:int = help.TimezonesList; quickReply#697102b shortcut_id:int shortcut:string top_message:int count:int = QuickReply; inputQuickReplyShortcut#24596d41 shortcut:string = InputQuickReplyShortcut; inputQuickReplyShortcutId#1190cf1 shortcut_id:int = InputQuickReplyShortcut; messages.quickReplies#c68d6695 quick_replies:Vector messages:Vector chats:Vector users:Vector = messages.QuickReplies; messages.quickRepliesNotModified#5f91eb5b = messages.QuickReplies; connectedBot#bd068601 flags:# can_reply:flags.0?true bot_id:long recipients:BusinessBotRecipients = ConnectedBot; account.connectedBots#17d7f87b connected_bots:Vector users:Vector = account.ConnectedBots; messages.dialogFilters#2ad93719 flags:# tags_enabled:flags.0?true filters:Vector = messages.DialogFilters; birthday#6c8e1e06 flags:# day:int month:int year:flags.0?int = Birthday; botBusinessConnection#896433b4 flags:# can_reply:flags.0?true disabled:flags.1?true connection_id:string user_id:long dc_id:int date:int = BotBusinessConnection; inputBusinessIntro#9c469cd flags:# title:string description:string sticker:flags.0?InputDocument = InputBusinessIntro; businessIntro#5a0a066d flags:# title:string description:string sticker:flags.0?Document = BusinessIntro; messages.myStickers#faff629d count:int sets:Vector = messages.MyStickers; inputCollectibleUsername#e39460a9 username:string = InputCollectible; inputCollectiblePhone#a2e214a4 phone:string = InputCollectible; fragment.collectibleInfo#6ebdff91 purchase_date:int currency:string amount:long crypto_currency:string crypto_amount:long url:string = fragment.CollectibleInfo; inputBusinessBotRecipients#c4e5921e flags:# existing_chats:flags.0?true new_chats:flags.1?true contacts:flags.2?true non_contacts:flags.3?true exclude_selected:flags.5?true users:flags.4?Vector exclude_users:flags.6?Vector = InputBusinessBotRecipients; businessBotRecipients#b88cf373 flags:# existing_chats:flags.0?true new_chats:flags.1?true contacts:flags.2?true non_contacts:flags.3?true exclude_selected:flags.5?true users:flags.4?Vector exclude_users:flags.6?Vector = BusinessBotRecipients; contactBirthday#1d998733 contact_id:long birthday:Birthday = ContactBirthday; contacts.contactBirthdays#114ff30d contacts:Vector users:Vector = contacts.ContactBirthdays; missingInvitee#628c9224 flags:# premium_would_allow_invite:flags.0?true premium_required_for_pm:flags.1?true user_id:long = MissingInvitee; messages.invitedUsers#7f5defa6 updates:Updates missing_invitees:Vector = messages.InvitedUsers; inputBusinessChatLink#11679fa7 flags:# message:string entities:flags.0?Vector title:flags.1?string = InputBusinessChatLink; businessChatLink#b4ae666f flags:# link:string message:string entities:flags.0?Vector title:flags.1?string views:int = BusinessChatLink; account.businessChatLinks#ec43a2d1 links:Vector chats:Vector users:Vector = account.BusinessChatLinks; account.resolvedBusinessChatLinks#9a23af21 flags:# peer:Peer message:string entities:flags.0?Vector chats:Vector users:Vector = account.ResolvedBusinessChatLinks; requestedPeerUser#d62ff46a flags:# user_id:long first_name:flags.0?string last_name:flags.0?string username:flags.1?string photo:flags.2?Photo = RequestedPeer; requestedPeerChat#7307544f flags:# chat_id:long title:flags.0?string photo:flags.2?Photo = RequestedPeer; requestedPeerChannel#8ba403e4 flags:# channel_id:long title:flags.0?string username:flags.1?string photo:flags.2?Photo = RequestedPeer; sponsoredMessageReportOption#430d3150 text:string option:bytes = SponsoredMessageReportOption; channels.sponsoredMessageReportResultChooseOption#846f9e42 title:string options:Vector = channels.SponsoredMessageReportResult; channels.sponsoredMessageReportResultAdsHidden#3e3bcf2f = channels.SponsoredMessageReportResult; channels.sponsoredMessageReportResultReported#ad798849 = channels.SponsoredMessageReportResult; stats.broadcastRevenueStats#5407e297 top_hours_graph:StatsGraph revenue_graph:StatsGraph balances:BroadcastRevenueBalances usd_rate:double = stats.BroadcastRevenueStats; stats.broadcastRevenueWithdrawalUrl#ec659737 url:string = stats.BroadcastRevenueWithdrawalUrl; broadcastRevenueTransactionProceeds#557e2cc4 amount:long from_date:int to_date:int = BroadcastRevenueTransaction; broadcastRevenueTransactionWithdrawal#5a590978 flags:# pending:flags.0?true failed:flags.2?true amount:long date:int provider:string transaction_date:flags.1?int transaction_url:flags.1?string = BroadcastRevenueTransaction; broadcastRevenueTransactionRefund#42d30d2e amount:long date:int provider:string = BroadcastRevenueTransaction; stats.broadcastRevenueTransactions#87158466 count:int transactions:Vector = stats.BroadcastRevenueTransactions; reactionNotificationsFromContacts#bac3a61a = ReactionNotificationsFrom; reactionNotificationsFromAll#4b9e22a0 = ReactionNotificationsFrom; reactionsNotifySettings#56e34970 flags:# messages_notify_from:flags.0?ReactionNotificationsFrom stories_notify_from:flags.1?ReactionNotificationsFrom sound:NotificationSound show_previews:Bool = ReactionsNotifySettings; broadcastRevenueBalances#c3ff71e7 flags:# withdrawal_enabled:flags.0?true current_balance:long available_balance:long overall_revenue:long = BroadcastRevenueBalances; availableEffect#93c3e27e flags:# premium_required:flags.2?true id:long emoticon:string static_icon_id:flags.0?long effect_sticker_id:long effect_animation_id:flags.1?long = AvailableEffect; messages.availableEffectsNotModified#d1ed9a5b = messages.AvailableEffects; messages.availableEffects#bddb616e hash:int effects:Vector documents:Vector = messages.AvailableEffects; factCheck#b89bfccf flags:# need_check:flags.0?true country:flags.1?string text:flags.1?TextWithEntities hash:long = FactCheck; starsTransactionPeerUnsupported#95f2bfe4 = StarsTransactionPeer; starsTransactionPeerAppStore#b457b375 = StarsTransactionPeer; starsTransactionPeerPlayMarket#7b560a0b = StarsTransactionPeer; starsTransactionPeerPremiumBot#250dbaf8 = StarsTransactionPeer; starsTransactionPeerFragment#e92fd902 = StarsTransactionPeer; starsTransactionPeer#d80da15d peer:Peer = StarsTransactionPeer; starsTransactionPeerAds#60682812 = StarsTransactionPeer; starsTransactionPeerAPI#f9677aad = StarsTransactionPeer; starsTopupOption#bd915c0 flags:# extended:flags.1?true stars:long store_product:flags.0?string currency:string amount:long = StarsTopupOption; starsTransaction#64dfc926 flags:# refund:flags.3?true pending:flags.4?true failed:flags.6?true gift:flags.10?true reaction:flags.11?true stargift_upgrade:flags.18?true id:string stars:StarsAmount date:int peer:StarsTransactionPeer title:flags.0?string description:flags.1?string photo:flags.2?WebDocument transaction_date:flags.5?int transaction_url:flags.5?string bot_payload:flags.7?bytes msg_id:flags.8?int extended_media:flags.9?Vector subscription_period:flags.12?int giveaway_post_id:flags.13?int stargift:flags.14?StarGift floodskip_number:flags.15?int starref_commission_permille:flags.16?int starref_peer:flags.17?Peer starref_amount:flags.17?StarsAmount = StarsTransaction; payments.starsStatus#6c9ce8ed flags:# balance:StarsAmount subscriptions:flags.1?Vector subscriptions_next_offset:flags.2?string subscriptions_missing_balance:flags.4?long history:flags.3?Vector next_offset:flags.0?string chats:Vector users:Vector = payments.StarsStatus; foundStory#e87acbc0 peer:Peer story:StoryItem = FoundStory; stories.foundStories#e2de7737 flags:# count:int stories:Vector next_offset:flags.0?string chats:Vector users:Vector = stories.FoundStories; geoPointAddress#de4c5d93 flags:# country_iso2:string state:flags.0?string city:flags.1?string street:flags.2?string = GeoPointAddress; starsRevenueStatus#febe5491 flags:# withdrawal_enabled:flags.0?true current_balance:StarsAmount available_balance:StarsAmount overall_revenue:StarsAmount next_withdrawal_at:flags.1?int = StarsRevenueStatus; payments.starsRevenueStats#c92bb73b revenue_graph:StatsGraph status:StarsRevenueStatus usd_rate:double = payments.StarsRevenueStats; payments.starsRevenueWithdrawalUrl#1dab80b7 url:string = payments.StarsRevenueWithdrawalUrl; payments.starsRevenueAdsAccountUrl#394e7f21 url:string = payments.StarsRevenueAdsAccountUrl; inputStarsTransaction#206ae6d1 flags:# refund:flags.0?true id:string = InputStarsTransaction; starsGiftOption#5e0589f1 flags:# extended:flags.1?true stars:long store_product:flags.0?string currency:string amount:long = StarsGiftOption; bots.popularAppBots#1991b13b flags:# next_offset:flags.0?string users:Vector = bots.PopularAppBots; botPreviewMedia#23e91ba3 date:int media:MessageMedia = BotPreviewMedia; bots.previewInfo#ca71d64 media:Vector lang_codes:Vector = bots.PreviewInfo; starsSubscriptionPricing#5416d58 period:int amount:long = StarsSubscriptionPricing; starsSubscription#2e6eab1a flags:# canceled:flags.0?true can_refulfill:flags.1?true missing_balance:flags.2?true bot_canceled:flags.7?true id:string peer:Peer until_date:int pricing:StarsSubscriptionPricing chat_invite_hash:flags.3?string title:flags.4?string photo:flags.5?WebDocument invoice_slug:flags.6?string = StarsSubscription; messageReactor#4ba3a95a flags:# top:flags.0?true my:flags.1?true anonymous:flags.2?true peer_id:flags.3?Peer count:int = MessageReactor; starsGiveawayOption#94ce852a flags:# extended:flags.0?true default:flags.1?true stars:long yearly_boosts:int store_product:flags.2?string currency:string amount:long winners:Vector = StarsGiveawayOption; starsGiveawayWinnersOption#54236209 flags:# default:flags.0?true users:int per_user_stars:long = StarsGiveawayWinnersOption; starGift#2cc73c8 flags:# limited:flags.0?true sold_out:flags.1?true birthday:flags.2?true id:long sticker:Document stars:long availability_remains:flags.0?int availability_total:flags.0?int convert_stars:long first_sale_date:flags.1?int last_sale_date:flags.1?int upgrade_stars:flags.3?long = StarGift; starGiftUnique#5c62d151 flags:# id:long title:string slug:string num:int owner_id:flags.0?Peer owner_name:flags.1?string owner_address:flags.2?string attributes:Vector availability_issued:int availability_total:int gift_address:flags.3?string = StarGift; payments.starGiftsNotModified#a388a368 = payments.StarGifts; payments.starGifts#901689ea hash:int gifts:Vector = payments.StarGifts; messageReportOption#7903e3d9 text:string option:bytes = MessageReportOption; reportResultChooseOption#f0e4e0b6 title:string options:Vector = ReportResult; reportResultAddComment#6f09ac31 flags:# optional:flags.0?true option:bytes = ReportResult; reportResultReported#8db33c4b = ReportResult; messages.botPreparedInlineMessage#8ecf0511 id:string expire_date:int = messages.BotPreparedInlineMessage; messages.preparedInlineMessage#ff57708d query_id:long result:BotInlineResult peer_types:Vector cache_time:int users:Vector = messages.PreparedInlineMessage; botAppSettings#c99b1950 flags:# placeholder_path:flags.0?bytes background_color:flags.1?int background_dark_color:flags.2?int header_color:flags.3?int header_dark_color:flags.4?int = BotAppSettings; starRefProgram#dd0c66f2 flags:# bot_id:long commission_permille:int duration_months:flags.0?int end_date:flags.1?int daily_revenue_per_user:flags.2?StarsAmount = StarRefProgram; connectedBotStarRef#19a13f71 flags:# revoked:flags.1?true url:string date:int bot_id:long commission_permille:int duration_months:flags.0?int participants:long revenue:long = ConnectedBotStarRef; payments.connectedStarRefBots#98d5ea1d count:int connected_bots:Vector users:Vector = payments.ConnectedStarRefBots; payments.suggestedStarRefBots#b4d5d859 flags:# count:int suggested_bots:Vector users:Vector next_offset:flags.0?string = payments.SuggestedStarRefBots; starsAmount#bbb6b4a3 amount:long nanos:int = StarsAmount; messages.foundStickersNotModified#6010c534 flags:# next_offset:flags.0?int = messages.FoundStickers; messages.foundStickers#82c9e290 flags:# next_offset:flags.0?int hash:long stickers:Vector = messages.FoundStickers; botVerifierSettings#b0cd6617 flags:# can_modify_custom_description:flags.1?true icon:long company:string custom_description:flags.0?string = BotVerifierSettings; botVerification#f93cd45c bot_id:long icon:long description:string = BotVerification; starGiftAttributeModel#39d99013 name:string document:Document rarity_permille:int = StarGiftAttribute; starGiftAttributePattern#13acff19 name:string document:Document rarity_permille:int = StarGiftAttribute; starGiftAttributeBackdrop#94271762 name:string center_color:int edge_color:int pattern_color:int text_color:int rarity_permille:int = StarGiftAttribute; starGiftAttributeOriginalDetails#e0bff26c flags:# sender_id:flags.0?Peer recipient_id:Peer date:int message:flags.1?TextWithEntities = StarGiftAttribute; payments.starGiftUpgradePreview#167bd90b sample_attributes:Vector = payments.StarGiftUpgradePreview; users.users#62d706b8 users:Vector = users.Users; users.usersSlice#315a4974 count:int users:Vector = users.Users; payments.uniqueStarGift#caa2f60b gift:StarGift users:Vector = payments.UniqueStarGift; messages.webPagePreview#b53e8b21 media:MessageMedia users:Vector = messages.WebPagePreview; savedStarGift#6056dba5 flags:# name_hidden:flags.0?true unsaved:flags.5?true refunded:flags.9?true can_upgrade:flags.10?true from_id:flags.1?Peer date:int gift:StarGift message:flags.2?TextWithEntities msg_id:flags.3?int saved_id:flags.11?long convert_stars:flags.4?long upgrade_stars:flags.6?long can_export_at:flags.7?int transfer_stars:flags.8?long = SavedStarGift; payments.savedStarGifts#95f389b1 flags:# count:int chat_notifications_enabled:flags.1?Bool gifts:Vector next_offset:flags.0?string chats:Vector users:Vector = payments.SavedStarGifts; inputSavedStarGiftUser#69279795 msg_id:int = InputSavedStarGift; inputSavedStarGiftChat#f101aa7f peer:InputPeer saved_id:long = InputSavedStarGift; payments.starGiftWithdrawalUrl#84aa3a9c url:string = payments.StarGiftWithdrawalUrl; paidReactionPrivacyDefault#206ad49e = PaidReactionPrivacy; paidReactionPrivacyAnonymous#1f0c1ad9 = PaidReactionPrivacy; paidReactionPrivacyPeer#dc6cfcf0 peer:InputPeer = PaidReactionPrivacy; ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; invokeAfterMsgs#3dc4b4f0 {X:Type} msg_ids:Vector query:!X = X; initConnection#c1cd5ea9 {X:Type} flags:# api_id:int device_model:string system_version:string app_version:string system_lang_code:string lang_pack:string lang_code:string proxy:flags.0?InputClientProxy params:flags.1?JSONValue query:!X = X; invokeWithLayer#da9b0d0d {X:Type} layer:int query:!X = X; invokeWithoutUpdates#bf9459b7 {X:Type} query:!X = X; invokeWithMessagesRange#365275f2 {X:Type} range:MessageRange query:!X = X; invokeWithTakeout#aca9fd2e {X:Type} takeout_id:long query:!X = X; invokeWithBusinessConnection#dd289f8e {X:Type} connection_id:string query:!X = X; invokeWithGooglePlayIntegrity#1df92984 {X:Type} nonce:string token:string query:!X = X; invokeWithApnsSecret#0dae54f8 {X:Type} nonce:string secret:string query:!X = X; invokeWithReCaptcha#adbb0f94 {X:Type} token:string query:!X = X; auth.sendCode#a677244f phone_number:string api_id:int api_hash:string settings:CodeSettings = auth.SentCode; auth.signUp#aac7b717 flags:# no_joined_notifications:flags.0?true phone_number:string phone_code_hash:string first_name:string last_name:string = auth.Authorization; auth.signIn#8d52a951 flags:# phone_number:string phone_code_hash:string phone_code:flags.0?string email_verification:flags.1?EmailVerification = auth.Authorization; auth.logOut#3e72ba19 = auth.LoggedOut; auth.resetAuthorizations#9fab0d1a = Bool; auth.exportAuthorization#e5bfffcd dc_id:int = auth.ExportedAuthorization; auth.importAuthorization#a57a7dad id:long bytes:bytes = auth.Authorization; auth.bindTempAuthKey#cdd42a05 perm_auth_key_id:long nonce:long expires_at:int encrypted_message:bytes = Bool; auth.importBotAuthorization#67a3ff2c flags:int api_id:int api_hash:string bot_auth_token:string = auth.Authorization; auth.checkPassword#d18b4d16 password:InputCheckPasswordSRP = auth.Authorization; auth.requestPasswordRecovery#d897bc66 = auth.PasswordRecovery; auth.recoverPassword#37096c70 flags:# code:string new_settings:flags.0?account.PasswordInputSettings = auth.Authorization; auth.resendCode#cae47523 flags:# phone_number:string phone_code_hash:string reason:flags.0?string = auth.SentCode; auth.cancelCode#1f040578 phone_number:string phone_code_hash:string = Bool; auth.dropTempAuthKeys#8e48a188 except_auth_keys:Vector = Bool; auth.exportLoginToken#b7e085fe api_id:int api_hash:string except_ids:Vector = auth.LoginToken; auth.importLoginToken#95ac5ce4 token:bytes = auth.LoginToken; auth.acceptLoginToken#e894ad4d token:bytes = Authorization; auth.checkRecoveryPassword#d36bf79 code:string = Bool; auth.importWebTokenAuthorization#2db873a9 api_id:int api_hash:string web_auth_token:string = auth.Authorization; auth.requestFirebaseSms#8e39261e flags:# phone_number:string phone_code_hash:string safety_net_token:flags.0?string play_integrity_token:flags.2?string ios_push_secret:flags.1?string = Bool; auth.resetLoginEmail#7e960193 phone_number:string phone_code_hash:string = auth.SentCode; auth.reportMissingCode#cb9deff6 phone_number:string phone_code_hash:string mnc:string = Bool; account.registerDevice#ec86017a flags:# no_muted:flags.0?true token_type:int token:string app_sandbox:Bool secret:bytes other_uids:Vector = Bool; account.unregisterDevice#6a0d3206 token_type:int token:string other_uids:Vector = Bool; account.updateNotifySettings#84be5b93 peer:InputNotifyPeer settings:InputPeerNotifySettings = Bool; account.getNotifySettings#12b3ad31 peer:InputNotifyPeer = PeerNotifySettings; account.resetNotifySettings#db7e1747 = Bool; account.updateProfile#78515775 flags:# first_name:flags.0?string last_name:flags.1?string about:flags.2?string = User; account.updateStatus#6628562c offline:Bool = Bool; account.getWallPapers#7967d36 hash:long = account.WallPapers; account.reportPeer#c5ba3d86 peer:InputPeer reason:ReportReason message:string = Bool; account.checkUsername#2714d86c username:string = Bool; account.updateUsername#3e0bdd7c username:string = User; account.getPrivacy#dadbc950 key:InputPrivacyKey = account.PrivacyRules; account.setPrivacy#c9f81ce8 key:InputPrivacyKey rules:Vector = account.PrivacyRules; account.deleteAccount#a2c0cf74 flags:# reason:string password:flags.0?InputCheckPasswordSRP = Bool; account.getAccountTTL#8fc711d = AccountDaysTTL; account.setAccountTTL#2442485e ttl:AccountDaysTTL = Bool; account.sendChangePhoneCode#82574ae5 phone_number:string settings:CodeSettings = auth.SentCode; account.changePhone#70c32edb phone_number:string phone_code_hash:string phone_code:string = User; account.updateDeviceLocked#38df3532 period:int = Bool; account.getAuthorizations#e320c158 = account.Authorizations; account.resetAuthorization#df77f3bc hash:long = Bool; account.getPassword#548a30f5 = account.Password; account.getPasswordSettings#9cd4eaf9 password:InputCheckPasswordSRP = account.PasswordSettings; account.updatePasswordSettings#a59b102f password:InputCheckPasswordSRP new_settings:account.PasswordInputSettings = Bool; account.sendConfirmPhoneCode#1b3faa88 hash:string settings:CodeSettings = auth.SentCode; account.confirmPhone#5f2178c3 phone_code_hash:string phone_code:string = Bool; account.getTmpPassword#449e0b51 password:InputCheckPasswordSRP period:int = account.TmpPassword; account.getWebAuthorizations#182e6d6f = account.WebAuthorizations; account.resetWebAuthorization#2d01b9ef hash:long = Bool; account.resetWebAuthorizations#682d2594 = Bool; account.getAllSecureValues#b288bc7d = Vector; account.getSecureValue#73665bc2 types:Vector = Vector; account.saveSecureValue#899fe31d value:InputSecureValue secure_secret_id:long = SecureValue; account.deleteSecureValue#b880bc4b types:Vector = Bool; account.getAuthorizationForm#a929597a bot_id:long scope:string public_key:string = account.AuthorizationForm; account.acceptAuthorization#f3ed4c73 bot_id:long scope:string public_key:string value_hashes:Vector credentials:SecureCredentialsEncrypted = Bool; account.sendVerifyPhoneCode#a5a356f9 phone_number:string settings:CodeSettings = auth.SentCode; account.verifyPhone#4dd3a7f6 phone_number:string phone_code_hash:string phone_code:string = Bool; account.sendVerifyEmailCode#98e037bb purpose:EmailVerifyPurpose email:string = account.SentEmailCode; account.verifyEmail#32da4cf purpose:EmailVerifyPurpose verification:EmailVerification = account.EmailVerified; account.initTakeoutSession#8ef3eab0 flags:# contacts:flags.0?true message_users:flags.1?true message_chats:flags.2?true message_megagroups:flags.3?true message_channels:flags.4?true files:flags.5?true file_max_size:flags.5?long = account.Takeout; account.finishTakeoutSession#1d2652ee flags:# success:flags.0?true = Bool; account.confirmPasswordEmail#8fdf1920 code:string = Bool; account.resendPasswordEmail#7a7f2a15 = Bool; account.cancelPasswordEmail#c1cbd5b6 = Bool; account.getContactSignUpNotification#9f07c728 = Bool; account.setContactSignUpNotification#cff43f61 silent:Bool = Bool; account.getNotifyExceptions#53577479 flags:# compare_sound:flags.1?true compare_stories:flags.2?true peer:flags.0?InputNotifyPeer = Updates; account.getWallPaper#fc8ddbea wallpaper:InputWallPaper = WallPaper; account.uploadWallPaper#e39a8f03 flags:# for_chat:flags.0?true file:InputFile mime_type:string settings:WallPaperSettings = WallPaper; account.saveWallPaper#6c5a5b37 wallpaper:InputWallPaper unsave:Bool settings:WallPaperSettings = Bool; account.installWallPaper#feed5769 wallpaper:InputWallPaper settings:WallPaperSettings = Bool; account.resetWallPapers#bb3b9804 = Bool; account.getAutoDownloadSettings#56da0b3f = account.AutoDownloadSettings; account.saveAutoDownloadSettings#76f36233 flags:# low:flags.0?true high:flags.1?true settings:AutoDownloadSettings = Bool; account.uploadTheme#1c3db333 flags:# file:InputFile thumb:flags.0?InputFile file_name:string mime_type:string = Document; account.createTheme#652e4400 flags:# slug:string title:string document:flags.2?InputDocument settings:flags.3?Vector = Theme; account.updateTheme#2bf40ccc flags:# format:string theme:InputTheme slug:flags.0?string title:flags.1?string document:flags.2?InputDocument settings:flags.3?Vector = Theme; account.saveTheme#f257106c theme:InputTheme unsave:Bool = Bool; account.installTheme#c727bb3b flags:# dark:flags.0?true theme:flags.1?InputTheme format:flags.2?string base_theme:flags.3?BaseTheme = Bool; account.getTheme#3a5869ec format:string theme:InputTheme = Theme; account.getThemes#7206e458 format:string hash:long = account.Themes; account.setContentSettings#b574b16b flags:# sensitive_enabled:flags.0?true = Bool; account.getContentSettings#8b9b4dae = account.ContentSettings; account.getMultiWallPapers#65ad71dc wallpapers:Vector = Vector; account.getGlobalPrivacySettings#eb2b4cf6 = GlobalPrivacySettings; account.setGlobalPrivacySettings#1edaaac2 settings:GlobalPrivacySettings = GlobalPrivacySettings; account.reportProfilePhoto#fa8cc6f5 peer:InputPeer photo_id:InputPhoto reason:ReportReason message:string = Bool; account.resetPassword#9308ce1b = account.ResetPasswordResult; account.declinePasswordReset#4c9409f6 = Bool; account.getChatThemes#d638de89 hash:long = account.Themes; account.setAuthorizationTTL#bf899aa0 authorization_ttl_days:int = Bool; account.changeAuthorizationSettings#40f48462 flags:# confirmed:flags.3?true hash:long encrypted_requests_disabled:flags.0?Bool call_requests_disabled:flags.1?Bool = Bool; account.getSavedRingtones#e1902288 hash:long = account.SavedRingtones; account.saveRingtone#3dea5b03 id:InputDocument unsave:Bool = account.SavedRingtone; account.uploadRingtone#831a83a2 file:InputFile file_name:string mime_type:string = Document; account.updateEmojiStatus#fbd3de6b emoji_status:EmojiStatus = Bool; account.getDefaultEmojiStatuses#d6753386 hash:long = account.EmojiStatuses; account.getRecentEmojiStatuses#f578105 hash:long = account.EmojiStatuses; account.clearRecentEmojiStatuses#18201aae = Bool; account.reorderUsernames#ef500eab order:Vector = Bool; account.toggleUsername#58d6b376 username:string active:Bool = Bool; account.getDefaultProfilePhotoEmojis#e2750328 hash:long = EmojiList; account.getDefaultGroupPhotoEmojis#915860ae hash:long = EmojiList; account.getAutoSaveSettings#adcbbcda = account.AutoSaveSettings; account.saveAutoSaveSettings#d69b8361 flags:# users:flags.0?true chats:flags.1?true broadcasts:flags.2?true peer:flags.3?InputPeer settings:AutoSaveSettings = Bool; account.deleteAutoSaveExceptions#53bc0020 = Bool; account.invalidateSignInCodes#ca8ae8ba codes:Vector = Bool; account.updateColor#7cefa15d flags:# for_profile:flags.1?true color:flags.2?int background_emoji_id:flags.0?long = Bool; account.getDefaultBackgroundEmojis#a60ab9ce hash:long = EmojiList; account.getChannelDefaultEmojiStatuses#7727a7d5 hash:long = account.EmojiStatuses; account.getChannelRestrictedStatusEmojis#35a9e0d5 hash:long = EmojiList; account.updateBusinessWorkHours#4b00e066 flags:# business_work_hours:flags.0?BusinessWorkHours = Bool; account.updateBusinessLocation#9e6b131a flags:# geo_point:flags.1?InputGeoPoint address:flags.0?string = Bool; account.updateBusinessGreetingMessage#66cdafc4 flags:# message:flags.0?InputBusinessGreetingMessage = Bool; account.updateBusinessAwayMessage#a26a7fa5 flags:# message:flags.0?InputBusinessAwayMessage = Bool; account.updateConnectedBot#43d8521d flags:# can_reply:flags.0?true deleted:flags.1?true bot:InputUser recipients:InputBusinessBotRecipients = Updates; account.getConnectedBots#4ea4c80f = account.ConnectedBots; account.getBotBusinessConnection#76a86270 connection_id:string = Updates; account.updateBusinessIntro#a614d034 flags:# intro:flags.0?InputBusinessIntro = Bool; account.toggleConnectedBotPaused#646e1097 peer:InputPeer paused:Bool = Bool; account.disablePeerConnectedBot#5e437ed9 peer:InputPeer = Bool; account.updateBirthday#cc6e0c11 flags:# birthday:flags.0?Birthday = Bool; account.createBusinessChatLink#8851e68e link:InputBusinessChatLink = BusinessChatLink; account.editBusinessChatLink#8c3410af slug:string link:InputBusinessChatLink = BusinessChatLink; account.deleteBusinessChatLink#60073674 slug:string = Bool; account.getBusinessChatLinks#6f70dde1 = account.BusinessChatLinks; account.resolveBusinessChatLink#5492e5ee slug:string = account.ResolvedBusinessChatLinks; account.updatePersonalChannel#d94305e0 channel:InputChannel = Bool; account.toggleSponsoredMessages#b9d9a38d enabled:Bool = Bool; account.getReactionsNotifySettings#6dd654c = ReactionsNotifySettings; account.setReactionsNotifySettings#316ce548 settings:ReactionsNotifySettings = ReactionsNotifySettings; account.getCollectibleEmojiStatuses#2e7b4543 hash:long = account.EmojiStatuses; users.getUsers#d91a548 id:Vector = Vector; users.getFullUser#b60f5918 id:InputUser = users.UserFull; users.setSecureValueErrors#90c894b5 id:InputUser errors:Vector = Bool; users.getIsPremiumRequiredToContact#a622aa10 id:Vector = Vector; contacts.getContactIDs#7adc669d hash:long = Vector; contacts.getStatuses#c4a353ee = Vector; contacts.getContacts#5dd69e12 hash:long = contacts.Contacts; contacts.importContacts#2c800be5 contacts:Vector = contacts.ImportedContacts; contacts.deleteContacts#96a0e00 id:Vector = Updates; contacts.deleteByPhones#1013fd9e phones:Vector = Bool; contacts.block#2e2e8734 flags:# my_stories_from:flags.0?true id:InputPeer = Bool; contacts.unblock#b550d328 flags:# my_stories_from:flags.0?true id:InputPeer = Bool; contacts.getBlocked#9a868f80 flags:# my_stories_from:flags.0?true offset:int limit:int = contacts.Blocked; contacts.search#11f812d8 q:string limit:int = contacts.Found; contacts.resolveUsername#725afbbc flags:# username:string referer:flags.0?string = contacts.ResolvedPeer; contacts.getTopPeers#973478b6 flags:# correspondents:flags.0?true bots_pm:flags.1?true bots_inline:flags.2?true phone_calls:flags.3?true forward_users:flags.4?true forward_chats:flags.5?true groups:flags.10?true channels:flags.15?true bots_app:flags.16?true offset:int limit:int hash:long = contacts.TopPeers; contacts.resetTopPeerRating#1ae373ac category:TopPeerCategory peer:InputPeer = Bool; contacts.resetSaved#879537f1 = Bool; contacts.getSaved#82f1e39f = Vector; contacts.toggleTopPeers#8514bdda enabled:Bool = Bool; contacts.addContact#e8f463d0 flags:# add_phone_privacy_exception:flags.0?true id:InputUser first_name:string last_name:string phone:string = Updates; contacts.acceptContact#f831a20f id:InputUser = Updates; contacts.getLocated#d348bc44 flags:# background:flags.1?true geo_point:InputGeoPoint self_expires:flags.0?int = Updates; contacts.blockFromReplies#29a8962c flags:# delete_message:flags.0?true delete_history:flags.1?true report_spam:flags.2?true msg_id:int = Updates; contacts.resolvePhone#8af94344 phone:string = contacts.ResolvedPeer; contacts.exportContactToken#f8654027 = ExportedContactToken; contacts.importContactToken#13005788 token:string = User; contacts.editCloseFriends#ba6705f0 id:Vector = Bool; contacts.setBlocked#94c65c76 flags:# my_stories_from:flags.0?true id:Vector limit:int = Bool; contacts.getBirthdays#daeda864 = contacts.ContactBirthdays; messages.getMessages#63c66506 id:Vector = messages.Messages; messages.getDialogs#a0f4cb4f flags:# exclude_pinned:flags.0?true folder_id:flags.1?int offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:long = messages.Dialogs; messages.getHistory#4423e6c5 peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages; messages.search#29ee847a flags:# peer:InputPeer q:string from_id:flags.0?InputPeer saved_peer_id:flags.2?InputPeer saved_reaction:flags.3?Vector top_msg_id:flags.1?int filter:MessagesFilter min_date:int max_date:int offset_id:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages; messages.readHistory#e306d3a peer:InputPeer max_id:int = messages.AffectedMessages; messages.deleteHistory#b08f922a flags:# just_clear:flags.0?true revoke:flags.1?true peer:InputPeer max_id:int min_date:flags.2?int max_date:flags.3?int = messages.AffectedHistory; messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector = messages.AffectedMessages; messages.receivedMessages#5a954c0 max_id:int = Vector; messages.setTyping#58943ee2 flags:# peer:InputPeer top_msg_id:flags.0?int action:SendMessageAction = Bool; messages.sendMessage#983f9745 flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long = Updates; messages.sendMedia#7852834e flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long = Updates; messages.forwardMessages#6d74da08 flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true allow_paid_floodskip:flags.19?true from_peer:InputPeer id:Vector random_id:Vector to_peer:InputPeer top_msg_id:flags.9?int schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut video_timestamp:flags.20?int = Updates; messages.reportSpam#cf1592db peer:InputPeer = Bool; messages.getPeerSettings#efd9a6a2 peer:InputPeer = messages.PeerSettings; messages.report#fc78af9b peer:InputPeer id:Vector option:bytes message:string = ReportResult; messages.getChats#49e9528f id:Vector = messages.Chats; messages.getFullChat#aeb00b34 chat_id:long = messages.ChatFull; messages.editChatTitle#73783ffd chat_id:long title:string = Updates; messages.editChatPhoto#35ddd674 chat_id:long photo:InputChatPhoto = Updates; messages.addChatUser#cbc6d107 chat_id:long user_id:InputUser fwd_limit:int = messages.InvitedUsers; messages.deleteChatUser#a2185cab flags:# revoke_history:flags.0?true chat_id:long user_id:InputUser = Updates; messages.createChat#92ceddd4 flags:# users:Vector title:string ttl_period:flags.0?int = messages.InvitedUsers; messages.getDhConfig#26cf8950 version:int random_length:int = messages.DhConfig; messages.requestEncryption#f64daf43 user_id:InputUser random_id:int g_a:bytes = EncryptedChat; messages.acceptEncryption#3dbc0415 peer:InputEncryptedChat g_b:bytes key_fingerprint:long = EncryptedChat; messages.discardEncryption#f393aea0 flags:# delete_history:flags.0?true chat_id:int = Bool; messages.setEncryptedTyping#791451ed peer:InputEncryptedChat typing:Bool = Bool; messages.readEncryptedHistory#7f4b690a peer:InputEncryptedChat max_date:int = Bool; messages.sendEncrypted#44fa7a15 flags:# silent:flags.0?true peer:InputEncryptedChat random_id:long data:bytes = messages.SentEncryptedMessage; messages.sendEncryptedFile#5559481d flags:# silent:flags.0?true peer:InputEncryptedChat random_id:long data:bytes file:InputEncryptedFile = messages.SentEncryptedMessage; messages.sendEncryptedService#32d439a4 peer:InputEncryptedChat random_id:long data:bytes = messages.SentEncryptedMessage; messages.receivedQueue#55a5bb66 max_qts:int = Vector; messages.reportEncryptedSpam#4b0c8c0f peer:InputEncryptedChat = Bool; messages.readMessageContents#36a73f77 id:Vector = messages.AffectedMessages; messages.getStickers#d5a5d3a1 emoticon:string hash:long = messages.Stickers; messages.getAllStickers#b8a0a1a8 hash:long = messages.AllStickers; messages.getWebPagePreview#570d6f6f flags:# message:string entities:flags.3?Vector = messages.WebPagePreview; messages.exportChatInvite#a455de90 flags:# legacy_revoke_permanent:flags.2?true request_needed:flags.3?true peer:InputPeer expire_date:flags.0?int usage_limit:flags.1?int title:flags.4?string subscription_pricing:flags.5?StarsSubscriptionPricing = ExportedChatInvite; messages.checkChatInvite#3eadb1bb hash:string = ChatInvite; messages.importChatInvite#6c50051c hash:string = Updates; messages.getStickerSet#c8a0ec74 stickerset:InputStickerSet hash:int = messages.StickerSet; messages.installStickerSet#c78fe460 stickerset:InputStickerSet archived:Bool = messages.StickerSetInstallResult; messages.uninstallStickerSet#f96e55de stickerset:InputStickerSet = Bool; messages.startBot#e6df7378 bot:InputUser peer:InputPeer random_id:long start_param:string = Updates; messages.getMessagesViews#5784d3e1 peer:InputPeer id:Vector increment:Bool = messages.MessageViews; messages.editChatAdmin#a85bd1c2 chat_id:long user_id:InputUser is_admin:Bool = Bool; messages.migrateChat#a2875319 chat_id:long = Updates; messages.searchGlobal#4bc6589a flags:# broadcasts_only:flags.1?true groups_only:flags.2?true users_only:flags.3?true folder_id:flags.0?int q:string filter:MessagesFilter min_date:int max_date:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; messages.reorderStickerSets#78337739 flags:# masks:flags.0?true emojis:flags.1?true order:Vector = Bool; messages.getDocumentByHash#b1f2061f sha256:bytes size:long mime_type:string = Document; messages.getSavedGifs#5cf09635 hash:long = messages.SavedGifs; messages.saveGif#327a30cb id:InputDocument unsave:Bool = Bool; messages.getInlineBotResults#514e999d flags:# bot:InputUser peer:InputPeer geo_point:flags.0?InputGeoPoint query:string offset:string = messages.BotResults; messages.setInlineBotResults#bb12a419 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 switch_webview:flags.4?InlineBotWebView = Bool; messages.sendInlineBotResult#3ebee86a flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true hide_via:flags.11?true peer:InputPeer reply_to:flags.0?InputReplyTo random_id:long query_id:long id:string schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut = Updates; messages.getMessageEditData#fda68d36 peer:InputPeer id:int = messages.MessageEditData; messages.editMessage#dfd14005 flags:# no_webpage:flags.1?true invert_media:flags.16?true peer:InputPeer id:int message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.15?int quick_reply_shortcut_id:flags.17?int = Updates; messages.editInlineBotMessage#83557dba flags:# no_webpage:flags.1?true invert_media:flags.16?true id:InputBotInlineMessageID message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector = Bool; messages.getBotCallbackAnswer#9342ca07 flags:# game:flags.1?true peer:InputPeer msg_id:int data:flags.0?bytes password:flags.2?InputCheckPasswordSRP = messages.BotCallbackAnswer; messages.setBotCallbackAnswer#d58f130a flags:# alert:flags.1?true query_id:long message:flags.0?string url:flags.2?string cache_time:int = Bool; messages.getPeerDialogs#e470bcfd peers:Vector = messages.PeerDialogs; messages.saveDraft#d372c5ce flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo peer:InputPeer message:string entities:flags.3?Vector media:flags.5?InputMedia effect:flags.7?long = Bool; messages.getAllDrafts#6a3f8d65 = Updates; messages.getFeaturedStickers#64780b14 hash:long = messages.FeaturedStickers; messages.readFeaturedStickers#5b118126 id:Vector = Bool; messages.getRecentStickers#9da9403b flags:# attached:flags.0?true hash:long = messages.RecentStickers; messages.saveRecentSticker#392718f8 flags:# attached:flags.0?true id:InputDocument unsave:Bool = Bool; messages.clearRecentStickers#8999602d flags:# attached:flags.0?true = Bool; messages.getArchivedStickers#57f17692 flags:# masks:flags.0?true emojis:flags.1?true offset_id:long limit:int = messages.ArchivedStickers; messages.getMaskStickers#640f82b8 hash:long = messages.AllStickers; messages.getAttachedStickers#cc5b67cc media:InputStickeredMedia = Vector; messages.setGameScore#8ef8ecc0 flags:# edit_message:flags.0?true force:flags.1?true peer:InputPeer id:int user_id:InputUser score:int = Updates; messages.setInlineGameScore#15ad9f64 flags:# edit_message:flags.0?true force:flags.1?true id:InputBotInlineMessageID user_id:InputUser score:int = Bool; messages.getGameHighScores#e822649d peer:InputPeer id:int user_id:InputUser = messages.HighScores; messages.getInlineGameHighScores#f635e1b id:InputBotInlineMessageID user_id:InputUser = messages.HighScores; messages.getCommonChats#e40ca104 user_id:InputUser max_id:long limit:int = messages.Chats; messages.getWebPage#8d9692a3 url:string hash:int = messages.WebPage; messages.toggleDialogPin#a731e257 flags:# pinned:flags.0?true peer:InputDialogPeer = Bool; messages.reorderPinnedDialogs#3b1adf37 flags:# force:flags.0?true folder_id:int order:Vector = Bool; messages.getPinnedDialogs#d6b94df2 folder_id:int = messages.PeerDialogs; messages.setBotShippingResults#e5f672fa flags:# query_id:long error:flags.0?string shipping_options:flags.1?Vector = Bool; messages.setBotPrecheckoutResults#9c2dd95 flags:# success:flags.1?true query_id:long error:flags.0?string = Bool; messages.uploadMedia#14967978 flags:# business_connection_id:flags.0?string peer:InputPeer media:InputMedia = MessageMedia; messages.sendScreenshotNotification#a1405817 peer:InputPeer reply_to:InputReplyTo random_id:long = Updates; messages.getFavedStickers#4f1aaa9 hash:long = messages.FavedStickers; messages.faveSticker#b9ffc55b id:InputDocument unfave:Bool = Bool; messages.getUnreadMentions#f107e790 flags:# peer:InputPeer top_msg_id:flags.0?int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; messages.readMentions#36e5bf4d flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory; messages.getRecentLocations#702a40e0 peer:InputPeer limit:int hash:long = messages.Messages; messages.sendMultiMedia#37b74355 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true invert_media:flags.16?true allow_paid_floodskip:flags.19?true peer:InputPeer reply_to:flags.0?InputReplyTo multi_media:Vector schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long = Updates; messages.uploadEncryptedFile#5057c497 peer:InputEncryptedChat file:InputEncryptedFile = EncryptedFile; messages.searchStickerSets#35705b8a flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets; messages.getSplitRanges#1cff7e08 = Vector; messages.markDialogUnread#c286d98f flags:# unread:flags.0?true peer:InputDialogPeer = Bool; messages.getDialogUnreadMarks#22e24e22 = Vector; messages.clearAllDrafts#7e58ee9c = Bool; messages.updatePinnedMessage#d2aaf7ec flags:# silent:flags.0?true unpin:flags.1?true pm_oneside:flags.2?true peer:InputPeer id:int = Updates; messages.sendVote#10ea6184 peer:InputPeer msg_id:int options:Vector = Updates; messages.getPollResults#73bb643b peer:InputPeer msg_id:int = Updates; messages.getOnlines#6e2be050 peer:InputPeer = ChatOnlines; messages.editChatAbout#def60797 peer:InputPeer about:string = Bool; messages.editChatDefaultBannedRights#a5866b41 peer:InputPeer banned_rights:ChatBannedRights = Updates; messages.getEmojiKeywords#35a0e062 lang_code:string = EmojiKeywordsDifference; messages.getEmojiKeywordsDifference#1508b6af lang_code:string from_version:int = EmojiKeywordsDifference; messages.getEmojiKeywordsLanguages#4e9963b2 lang_codes:Vector = Vector; messages.getEmojiURL#d5b10c26 lang_code:string = EmojiURL; messages.getSearchCounters#1bbcf300 flags:# peer:InputPeer saved_peer_id:flags.2?InputPeer top_msg_id:flags.0?int filters:Vector = Vector; messages.requestUrlAuth#198fb446 flags:# peer:flags.1?InputPeer msg_id:flags.1?int button_id:flags.1?int url:flags.2?string = UrlAuthResult; messages.acceptUrlAuth#b12c7125 flags:# write_allowed:flags.0?true peer:flags.1?InputPeer msg_id:flags.1?int button_id:flags.1?int url:flags.2?string = UrlAuthResult; messages.hidePeerSettingsBar#4facb138 peer:InputPeer = Bool; messages.getScheduledHistory#f516760b peer:InputPeer hash:long = messages.Messages; messages.getScheduledMessages#bdbb0464 peer:InputPeer id:Vector = messages.Messages; messages.sendScheduledMessages#bd38850a peer:InputPeer id:Vector = Updates; messages.deleteScheduledMessages#59ae2b16 peer:InputPeer id:Vector = Updates; messages.getPollVotes#b86e380e flags:# peer:InputPeer id:int option:flags.0?bytes offset:flags.1?string limit:int = messages.VotesList; messages.toggleStickerSets#b5052fea flags:# uninstall:flags.0?true archive:flags.1?true unarchive:flags.2?true stickersets:Vector = Bool; messages.getDialogFilters#efd48c89 = messages.DialogFilters; messages.getSuggestedDialogFilters#a29cd42c = Vector; messages.updateDialogFilter#1ad4a04a flags:# id:int filter:flags.0?DialogFilter = Bool; messages.updateDialogFiltersOrder#c563c1e4 order:Vector = Bool; messages.getOldFeaturedStickers#7ed094a1 offset:int limit:int hash:long = messages.FeaturedStickers; messages.getReplies#22ddd30c peer:InputPeer msg_id:int offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages; messages.getDiscussionMessage#446972fd peer:InputPeer msg_id:int = messages.DiscussionMessage; messages.readDiscussion#f731a9f4 peer:InputPeer msg_id:int read_max_id:int = Bool; messages.unpinAllMessages#ee22b9a8 flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory; messages.deleteChat#5bd0ee50 chat_id:long = Bool; messages.deletePhoneCallHistory#f9cbe409 flags:# revoke:flags.0?true = messages.AffectedFoundMessages; messages.checkHistoryImport#43fe19f3 import_head:string = messages.HistoryImportParsed; messages.initHistoryImport#34090c3b peer:InputPeer file:InputFile media_count:int = messages.HistoryImport; messages.uploadImportedMedia#2a862092 peer:InputPeer import_id:long file_name:string media:InputMedia = MessageMedia; messages.startHistoryImport#b43df344 peer:InputPeer import_id:long = Bool; messages.getExportedChatInvites#a2b5a3f6 flags:# revoked:flags.3?true peer:InputPeer admin_id:InputUser offset_date:flags.2?int offset_link:flags.2?string limit:int = messages.ExportedChatInvites; messages.getExportedChatInvite#73746f5c peer:InputPeer link:string = messages.ExportedChatInvite; messages.editExportedChatInvite#bdca2f75 flags:# revoked:flags.2?true peer:InputPeer link:string expire_date:flags.0?int usage_limit:flags.1?int request_needed:flags.3?Bool title:flags.4?string = messages.ExportedChatInvite; messages.deleteRevokedExportedChatInvites#56987bd5 peer:InputPeer admin_id:InputUser = Bool; messages.deleteExportedChatInvite#d464a42b peer:InputPeer link:string = Bool; messages.getAdminsWithInvites#3920e6ef peer:InputPeer = messages.ChatAdminsWithInvites; messages.getChatInviteImporters#df04dd4e flags:# requested:flags.0?true subscription_expired:flags.3?true peer:InputPeer link:flags.1?string q:flags.2?string offset_date:int offset_user:InputUser limit:int = messages.ChatInviteImporters; messages.setHistoryTTL#b80e5fe4 peer:InputPeer period:int = Updates; messages.checkHistoryImportPeer#5dc60f03 peer:InputPeer = messages.CheckedHistoryImportPeer; messages.setChatTheme#e63be13f peer:InputPeer emoticon:string = Updates; messages.getMessageReadParticipants#31c1c44f peer:InputPeer msg_id:int = Vector; messages.getSearchResultsCalendar#6aa3f6bd flags:# peer:InputPeer saved_peer_id:flags.2?InputPeer filter:MessagesFilter offset_id:int offset_date:int = messages.SearchResultsCalendar; messages.getSearchResultsPositions#9c7f2f10 flags:# peer:InputPeer saved_peer_id:flags.2?InputPeer filter:MessagesFilter offset_id:int limit:int = messages.SearchResultsPositions; messages.hideChatJoinRequest#7fe7e815 flags:# approved:flags.0?true peer:InputPeer user_id:InputUser = Updates; messages.hideAllChatJoinRequests#e085f4ea flags:# approved:flags.0?true peer:InputPeer link:flags.1?string = Updates; messages.toggleNoForwards#b11eafa2 peer:InputPeer enabled:Bool = Updates; messages.saveDefaultSendAs#ccfddf96 peer:InputPeer send_as:InputPeer = Bool; messages.sendReaction#d30d78d4 flags:# big:flags.1?true add_to_recent:flags.2?true peer:InputPeer msg_id:int reaction:flags.0?Vector = Updates; messages.getMessagesReactions#8bba90e6 peer:InputPeer id:Vector = Updates; messages.getMessageReactionsList#461b3f48 flags:# peer:InputPeer id:int reaction:flags.0?Reaction offset:flags.1?string limit:int = messages.MessageReactionsList; messages.setChatAvailableReactions#864b2581 flags:# peer:InputPeer available_reactions:ChatReactions reactions_limit:flags.0?int paid_enabled:flags.1?Bool = Updates; messages.getAvailableReactions#18dea0ac hash:int = messages.AvailableReactions; messages.setDefaultReaction#4f47a016 reaction:Reaction = Bool; messages.translateText#63183030 flags:# peer:flags.0?InputPeer id:flags.0?Vector text:flags.1?Vector to_lang:string = messages.TranslatedText; messages.getUnreadReactions#3223495b flags:# peer:InputPeer top_msg_id:flags.0?int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; messages.readReactions#54aa7f8e flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory; messages.searchSentMedia#107e31a0 q:string filter:MessagesFilter limit:int = messages.Messages; messages.getAttachMenuBots#16fcc2cb hash:long = AttachMenuBots; messages.getAttachMenuBot#77216192 bot:InputUser = AttachMenuBotsBot; messages.toggleBotInAttachMenu#69f59d69 flags:# write_allowed:flags.0?true bot:InputUser enabled:Bool = Bool; messages.requestWebView#269dc2c1 flags:# from_bot_menu:flags.4?true silent:flags.5?true compact:flags.7?true fullscreen:flags.8?true peer:InputPeer bot:InputUser url:flags.1?string start_param:flags.3?string theme_params:flags.2?DataJSON platform:string reply_to:flags.0?InputReplyTo send_as:flags.13?InputPeer = WebViewResult; messages.prolongWebView#b0d81a83 flags:# silent:flags.5?true peer:InputPeer bot:InputUser query_id:long reply_to:flags.0?InputReplyTo send_as:flags.13?InputPeer = Bool; messages.requestSimpleWebView#413a3e73 flags:# from_switch_webview:flags.1?true from_side_menu:flags.2?true compact:flags.7?true fullscreen:flags.8?true bot:InputUser url:flags.3?string start_param:flags.4?string theme_params:flags.0?DataJSON platform:string = WebViewResult; messages.sendWebViewResultMessage#a4314f5 bot_query_id:string result:InputBotInlineResult = WebViewMessageSent; messages.sendWebViewData#dc0242c8 bot:InputUser random_id:long button_text:string data:string = Updates; messages.transcribeAudio#269e9a49 peer:InputPeer msg_id:int = messages.TranscribedAudio; messages.rateTranscribedAudio#7f1d072f peer:InputPeer msg_id:int transcription_id:long good:Bool = Bool; messages.getCustomEmojiDocuments#d9ab0f54 document_id:Vector = Vector; messages.getEmojiStickers#fbfca18f hash:long = messages.AllStickers; messages.getFeaturedEmojiStickers#ecf6736 hash:long = messages.FeaturedStickers; messages.reportReaction#3f64c076 peer:InputPeer id:int reaction_peer:InputPeer = Bool; messages.getTopReactions#bb8125ba limit:int hash:long = messages.Reactions; messages.getRecentReactions#39461db2 limit:int hash:long = messages.Reactions; messages.clearRecentReactions#9dfeefb4 = Bool; messages.getExtendedMedia#84f80814 peer:InputPeer id:Vector = Updates; messages.setDefaultHistoryTTL#9eb51445 period:int = Bool; messages.getDefaultHistoryTTL#658b7188 = DefaultHistoryTTL; messages.sendBotRequestedPeer#91b2d060 peer:InputPeer msg_id:int button_id:int requested_peers:Vector = Updates; messages.getEmojiGroups#7488ce5b hash:int = messages.EmojiGroups; messages.getEmojiStatusGroups#2ecd56cd hash:int = messages.EmojiGroups; messages.getEmojiProfilePhotoGroups#21a548f3 hash:int = messages.EmojiGroups; messages.searchCustomEmoji#2c11c0d7 emoticon:string hash:long = EmojiList; messages.togglePeerTranslations#e47cb579 flags:# disabled:flags.0?true peer:InputPeer = Bool; messages.getBotApp#34fdc5c3 app:InputBotApp hash:long = messages.BotApp; messages.requestAppWebView#53618bce flags:# write_allowed:flags.0?true compact:flags.7?true fullscreen:flags.8?true peer:InputPeer app:InputBotApp start_param:flags.1?string theme_params:flags.2?DataJSON platform:string = WebViewResult; messages.setChatWallPaper#8ffacae1 flags:# for_both:flags.3?true revert:flags.4?true peer:InputPeer wallpaper:flags.0?InputWallPaper settings:flags.2?WallPaperSettings id:flags.1?int = Updates; messages.searchEmojiStickerSets#92b4494c flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets; messages.getSavedDialogs#5381d21a flags:# exclude_pinned:flags.0?true offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:long = messages.SavedDialogs; messages.getSavedHistory#3d9a414d peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages; messages.deleteSavedHistory#6e98102b flags:# peer:InputPeer max_id:int min_date:flags.2?int max_date:flags.3?int = messages.AffectedHistory; messages.getPinnedSavedDialogs#d63d94e0 = messages.SavedDialogs; messages.toggleSavedDialogPin#ac81bbde flags:# pinned:flags.0?true peer:InputDialogPeer = Bool; messages.reorderPinnedSavedDialogs#8b716587 flags:# force:flags.0?true order:Vector = Bool; messages.getSavedReactionTags#3637e05b flags:# peer:flags.0?InputPeer hash:long = messages.SavedReactionTags; messages.updateSavedReactionTag#60297dec flags:# reaction:Reaction title:flags.0?string = Bool; messages.getDefaultTagReactions#bdf93428 hash:long = messages.Reactions; messages.getOutboxReadDate#8c4bfe5d peer:InputPeer msg_id:int = OutboxReadDate; messages.getQuickReplies#d483f2a8 hash:long = messages.QuickReplies; messages.reorderQuickReplies#60331907 order:Vector = Bool; messages.checkQuickReplyShortcut#f1d0fbd3 shortcut:string = Bool; messages.editQuickReplyShortcut#5c003cef shortcut_id:int shortcut:string = Bool; messages.deleteQuickReplyShortcut#3cc04740 shortcut_id:int = Bool; messages.getQuickReplyMessages#94a495c3 flags:# shortcut_id:int id:flags.0?Vector hash:long = messages.Messages; messages.sendQuickReplyMessages#6c750de1 peer:InputPeer shortcut_id:int id:Vector random_id:Vector = Updates; messages.deleteQuickReplyMessages#e105e910 shortcut_id:int id:Vector = Updates; messages.toggleDialogFilterTags#fd2dda49 enabled:Bool = Bool; messages.getMyStickers#d0b5e1fc offset_id:long limit:int = messages.MyStickers; messages.getEmojiStickerGroups#1dd840f5 hash:int = messages.EmojiGroups; messages.getAvailableEffects#dea20a39 hash:int = messages.AvailableEffects; messages.editFactCheck#589ee75 peer:InputPeer msg_id:int text:TextWithEntities = Updates; messages.deleteFactCheck#d1da940c peer:InputPeer msg_id:int = Updates; messages.getFactCheck#b9cdc5ee peer:InputPeer msg_id:Vector = Vector; messages.requestMainWebView#c9e01e7b flags:# compact:flags.7?true fullscreen:flags.8?true peer:InputPeer bot:InputUser start_param:flags.1?string theme_params:flags.0?DataJSON platform:string = WebViewResult; messages.sendPaidReaction#58bbcb50 flags:# peer:InputPeer msg_id:int count:int random_id:long private:flags.0?PaidReactionPrivacy = Updates; messages.togglePaidReactionPrivacy#435885b5 peer:InputPeer msg_id:int private:PaidReactionPrivacy = Bool; messages.getPaidReactionPrivacy#472455aa = Updates; messages.viewSponsoredMessage#673ad8f1 peer:InputPeer random_id:bytes = Bool; messages.clickSponsoredMessage#f093465 flags:# media:flags.0?true fullscreen:flags.1?true peer:InputPeer random_id:bytes = Bool; messages.reportSponsoredMessage#1af3dbb8 peer:InputPeer random_id:bytes option:bytes = channels.SponsoredMessageReportResult; messages.getSponsoredMessages#9bd2f439 peer:InputPeer = messages.SponsoredMessages; messages.savePreparedInlineMessage#f21f7f2f flags:# result:InputBotInlineResult user_id:InputUser peer_types:flags.0?Vector = messages.BotPreparedInlineMessage; messages.getPreparedInlineMessage#857ebdb8 bot:InputUser id:string = messages.PreparedInlineMessage; messages.searchStickers#29b1c66a flags:# emojis:flags.0?true q:string emoticon:string lang_code:Vector offset:int limit:int hash:long = messages.FoundStickers; messages.reportMessagesDelivery#5a6d7395 flags:# push:flags.0?true peer:InputPeer id:Vector = Bool; updates.getState#edd4882a = updates.State; updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference; updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference; photos.updateProfilePhoto#9e82039 flags:# fallback:flags.0?true bot:flags.1?InputUser id:InputPhoto = photos.Photo; photos.uploadProfilePhoto#388a3b5 flags:# fallback:flags.3?true bot:flags.5?InputUser file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double video_emoji_markup:flags.4?VideoSize = photos.Photo; photos.deletePhotos#87cf7f2f id:Vector = Vector; photos.getUserPhotos#91cd32a8 user_id:InputUser offset:int max_id:long limit:int = photos.Photos; photos.uploadContactProfilePhoto#e14c4a71 flags:# suggest:flags.3?true save:flags.4?true user_id:InputUser file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double video_emoji_markup:flags.5?VideoSize = photos.Photo; upload.saveFilePart#b304a621 file_id:long file_part:int bytes:bytes = Bool; upload.getFile#be5335be flags:# precise:flags.0?true cdn_supported:flags.1?true location:InputFileLocation offset:long limit:int = upload.File; upload.saveBigFilePart#de7b673d file_id:long file_part:int file_total_parts:int bytes:bytes = Bool; upload.getWebFile#24e6818d location:InputWebFileLocation offset:int limit:int = upload.WebFile; upload.getCdnFile#395f69da file_token:bytes offset:long limit:int = upload.CdnFile; upload.reuploadCdnFile#9b2754a8 file_token:bytes request_token:bytes = Vector; upload.getCdnFileHashes#91dc3f31 file_token:bytes offset:long = Vector; upload.getFileHashes#9156982a location:InputFileLocation offset:long = Vector; help.getConfig#c4f9186b = Config; help.getNearestDc#1fb33026 = NearestDc; help.getAppUpdate#522d5a7d source:string = help.AppUpdate; help.getInviteText#4d392343 = help.InviteText; help.getSupport#9cdf08cd = help.Support; help.setBotUpdatesStatus#ec22cfcd pending_updates_count:int message:string = Bool; help.getCdnConfig#52029342 = CdnConfig; help.getRecentMeUrls#3dc0f114 referer:string = help.RecentMeUrls; help.getTermsOfServiceUpdate#2ca51fd1 = help.TermsOfServiceUpdate; help.acceptTermsOfService#ee72f79a id:DataJSON = Bool; help.getDeepLinkInfo#3fedc75f path:string = help.DeepLinkInfo; help.getAppConfig#61e3f854 hash:int = help.AppConfig; help.saveAppLog#6f02f748 events:Vector = Bool; help.getPassportConfig#c661ad08 hash:int = help.PassportConfig; help.getSupportName#d360e72c = help.SupportName; help.getUserInfo#38a08d3 user_id:InputUser = help.UserInfo; help.editUserInfo#66b91b70 user_id:InputUser message:string entities:Vector = help.UserInfo; help.getPromoData#c0977421 = help.PromoData; help.hidePromoData#1e251c95 peer:InputPeer = Bool; help.dismissSuggestion#f50dbaa1 peer:InputPeer suggestion:string = Bool; help.getCountriesList#735787a8 lang_code:string hash:int = help.CountriesList; help.getPremiumPromo#b81b93d4 = help.PremiumPromo; help.getPeerColors#da80f42f hash:int = help.PeerColors; help.getPeerProfileColors#abcfa9fd hash:int = help.PeerColors; help.getTimezonesList#49b30240 hash:int = help.TimezonesList; channels.readHistory#cc104937 channel:InputChannel max_id:int = Bool; channels.deleteMessages#84c1fd4e channel:InputChannel id:Vector = messages.AffectedMessages; channels.reportSpam#f44a8315 channel:InputChannel participant:InputPeer id:Vector = Bool; channels.getMessages#ad8c9a23 channel:InputChannel id:Vector = messages.Messages; channels.getParticipants#77ced9d0 channel:InputChannel filter:ChannelParticipantsFilter offset:int limit:int hash:long = channels.ChannelParticipants; channels.getParticipant#a0ab6cc6 channel:InputChannel participant:InputPeer = channels.ChannelParticipant; channels.getChannels#a7f6bbb id:Vector = messages.Chats; channels.getFullChannel#8736a09 channel:InputChannel = messages.ChatFull; channels.createChannel#91006707 flags:# broadcast:flags.0?true megagroup:flags.1?true for_import:flags.3?true forum:flags.5?true title:string about:string geo_point:flags.2?InputGeoPoint address:flags.2?string ttl_period:flags.4?int = Updates; channels.editAdmin#d33c8902 channel:InputChannel user_id:InputUser admin_rights:ChatAdminRights rank:string = Updates; channels.editTitle#566decd0 channel:InputChannel title:string = Updates; channels.editPhoto#f12e57c9 channel:InputChannel photo:InputChatPhoto = Updates; channels.checkUsername#10e6bd2c channel:InputChannel username:string = Bool; channels.updateUsername#3514b3de channel:InputChannel username:string = Bool; channels.joinChannel#24b524c5 channel:InputChannel = Updates; channels.leaveChannel#f836aa95 channel:InputChannel = Updates; channels.inviteToChannel#c9e33d54 channel:InputChannel users:Vector = messages.InvitedUsers; channels.deleteChannel#c0111fe3 channel:InputChannel = Updates; channels.exportMessageLink#e63fadeb flags:# grouped:flags.0?true thread:flags.1?true channel:InputChannel id:int = ExportedMessageLink; channels.toggleSignatures#418d549c flags:# signatures_enabled:flags.0?true profiles_enabled:flags.1?true channel:InputChannel = Updates; channels.getAdminedPublicChannels#f8b036af flags:# by_location:flags.0?true check_limit:flags.1?true for_personal:flags.2?true = messages.Chats; channels.editBanned#96e6cd81 channel:InputChannel participant:InputPeer banned_rights:ChatBannedRights = Updates; 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#9baa9647 flags:# for_everyone:flags.0?true channel:InputChannel max_id:int = Updates; channels.togglePreHistoryHidden#eabbb94c channel:InputChannel enabled:Bool = Updates; channels.getLeftChannels#8341ecc0 offset:int = messages.Chats; channels.getGroupsForDiscussion#f5dad378 = messages.Chats; channels.setDiscussionGroup#40582bb2 broadcast:InputChannel group:InputChannel = Bool; channels.editCreator#8f38cd1f channel:InputChannel user_id:InputUser password:InputCheckPasswordSRP = Updates; channels.editLocation#58e63f6d channel:InputChannel geo_point:InputGeoPoint address:string = Bool; channels.toggleSlowMode#edd49ef0 channel:InputChannel seconds:int = Updates; channels.getInactiveChannels#11e831ee = messages.InactiveChats; channels.convertToGigagroup#b290c69 channel:InputChannel = Updates; channels.getSendAs#e785a43f flags:# for_paid_reactions:flags.0?true peer:InputPeer = channels.SendAsPeers; channels.deleteParticipantHistory#367544db channel:InputChannel participant:InputPeer = messages.AffectedHistory; channels.toggleJoinToSend#e4cb9580 channel:InputChannel enabled:Bool = Updates; channels.toggleJoinRequest#4c2985b6 channel:InputChannel enabled:Bool = Updates; channels.reorderUsernames#b45ced1d channel:InputChannel order:Vector = Bool; channels.toggleUsername#50f24105 channel:InputChannel username:string active:Bool = Bool; channels.deactivateAllUsernames#a245dd3 channel:InputChannel = Bool; channels.toggleForum#a4298b29 channel:InputChannel enabled:Bool = Updates; channels.createForumTopic#f40c0224 flags:# channel:InputChannel title:string icon_color:flags.0?int icon_emoji_id:flags.3?long random_id:long send_as:flags.2?InputPeer = Updates; channels.getForumTopics#de560d1 flags:# channel:InputChannel q:flags.0?string offset_date:int offset_id:int offset_topic:int limit:int = messages.ForumTopics; channels.getForumTopicsByID#b0831eb9 channel:InputChannel topics:Vector = messages.ForumTopics; channels.editForumTopic#f4dfa185 flags:# channel:InputChannel topic_id:int title:flags.0?string icon_emoji_id:flags.1?long closed:flags.2?Bool hidden:flags.3?Bool = Updates; channels.updatePinnedForumTopic#6c2d9026 channel:InputChannel topic_id:int pinned:Bool = Updates; channels.deleteTopicHistory#34435f2d channel:InputChannel top_msg_id:int = messages.AffectedHistory; channels.reorderPinnedForumTopics#2950a18f flags:# force:flags.0?true channel:InputChannel order:Vector = Updates; channels.toggleAntiSpam#68f3e4eb channel:InputChannel enabled:Bool = Updates; channels.reportAntiSpamFalsePositive#a850a693 channel:InputChannel msg_id:int = Bool; channels.toggleParticipantsHidden#6a6e7854 channel:InputChannel enabled:Bool = Updates; channels.updateColor#d8aa3671 flags:# for_profile:flags.1?true channel:InputChannel color:flags.2?int background_emoji_id:flags.0?long = Updates; channels.toggleViewForumAsMessages#9738bb15 channel:InputChannel enabled:Bool = Updates; channels.getChannelRecommendations#25a71742 flags:# channel:flags.0?InputChannel = messages.Chats; channels.updateEmojiStatus#f0d3e6a8 channel:InputChannel emoji_status:EmojiStatus = Updates; channels.setBoostsToUnblockRestrictions#ad399cee channel:InputChannel boosts:int = Updates; channels.setEmojiStickers#3cd930b7 channel:InputChannel stickerset:InputStickerSet = Bool; channels.restrictSponsoredMessages#9ae91519 channel:InputChannel restricted:Bool = Updates; channels.searchPosts#d19f987b hashtag:string offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON; bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool; bots.setBotCommands#517165a scope:BotCommandScope lang_code:string commands:Vector = Bool; bots.resetBotCommands#3d8de0f9 scope:BotCommandScope lang_code:string = Bool; bots.getBotCommands#e34c0dd6 scope:BotCommandScope lang_code:string = Vector; bots.setBotMenuButton#4504d54f user_id:InputUser button:BotMenuButton = Bool; bots.getBotMenuButton#9c60eb28 user_id:InputUser = BotMenuButton; bots.setBotBroadcastDefaultAdminRights#788464e1 admin_rights:ChatAdminRights = Bool; bots.setBotGroupDefaultAdminRights#925ec9ea admin_rights:ChatAdminRights = Bool; bots.setBotInfo#10cf3123 flags:# bot:flags.2?InputUser lang_code:string name:flags.3?string about:flags.0?string description:flags.1?string = Bool; bots.getBotInfo#dcd914fd flags:# bot:flags.0?InputUser lang_code:string = bots.BotInfo; bots.reorderUsernames#9709b1c2 bot:InputUser order:Vector = Bool; bots.toggleUsername#53ca973 bot:InputUser username:string active:Bool = Bool; bots.canSendMessage#1359f4e6 bot:InputUser = Bool; bots.allowSendMessage#f132e3ef bot:InputUser = Updates; bots.invokeWebViewCustomMethod#87fc5e7 bot:InputUser custom_method:string params:DataJSON = DataJSON; bots.getPopularAppBots#c2510192 offset:string limit:int = bots.PopularAppBots; bots.addPreviewMedia#17aeb75a bot:InputUser lang_code:string media:InputMedia = BotPreviewMedia; bots.editPreviewMedia#8525606f bot:InputUser lang_code:string media:InputMedia new_media:InputMedia = BotPreviewMedia; bots.deletePreviewMedia#2d0135b3 bot:InputUser lang_code:string media:Vector = Bool; bots.reorderPreviewMedias#b627f3aa bot:InputUser lang_code:string order:Vector = Bool; bots.getPreviewInfo#423ab3ad bot:InputUser lang_code:string = bots.PreviewInfo; bots.getPreviewMedias#a2a5594d bot:InputUser = Vector; bots.updateUserEmojiStatus#ed9f30c5 user_id:InputUser emoji_status:EmojiStatus = Bool; bots.toggleUserEmojiStatusPermission#6de6392 bot:InputUser enabled:Bool = Bool; bots.checkDownloadFileParams#50077589 bot:InputUser file_name:string url:string = Bool; bots.getAdminedBots#b0711d83 = Vector; bots.updateStarRefProgram#778b5ab3 flags:# bot:InputUser commission_permille:int duration_months:flags.0?int = StarRefProgram; bots.setCustomVerification#8b89dfbd flags:# enabled:flags.1?true bot:flags.0?InputUser peer:InputPeer custom_description:flags.2?string = Bool; bots.getBotRecommendations#a1b70815 bot:InputUser = users.Users; payments.getPaymentForm#37148dbb flags:# invoice:InputInvoice theme_params:flags.0?DataJSON = payments.PaymentForm; payments.getPaymentReceipt#2478d1cc peer:InputPeer msg_id:int = payments.PaymentReceipt; payments.validateRequestedInfo#b6c8f12b flags:# save:flags.0?true invoice:InputInvoice info:PaymentRequestedInfo = payments.ValidatedRequestedInfo; payments.sendPaymentForm#2d03522f flags:# form_id:long invoice:InputInvoice requested_info_id:flags.0?string shipping_option_id:flags.1?string credentials:InputPaymentCredentials tip_amount:flags.2?long = payments.PaymentResult; payments.getSavedInfo#227d824b = payments.SavedInfo; payments.clearSavedInfo#d83d70c1 flags:# credentials:flags.0?true info:flags.1?true = Bool; payments.getBankCardData#2e79d779 number:string = payments.BankCardData; payments.exportInvoice#f91b065 invoice_media:InputMedia = payments.ExportedInvoice; payments.assignAppStoreTransaction#80ed747d receipt:bytes purpose:InputStorePaymentPurpose = Updates; payments.assignPlayMarketTransaction#dffd50d3 receipt:DataJSON purpose:InputStorePaymentPurpose = Updates; payments.canPurchasePremium#9fc19eb6 purpose:InputStorePaymentPurpose = Bool; payments.getPremiumGiftCodeOptions#2757ba54 flags:# boost_peer:flags.0?InputPeer = Vector; payments.checkGiftCode#8e51b4c1 slug:string = payments.CheckedGiftCode; payments.applyGiftCode#f6e26854 slug:string = Updates; payments.getGiveawayInfo#f4239425 peer:InputPeer msg_id:int = payments.GiveawayInfo; payments.launchPrepaidGiveaway#5ff58f20 peer:InputPeer giveaway_id:long purpose:InputStorePaymentPurpose = Updates; payments.getStarsTopupOptions#c00ec7d3 = Vector; payments.getStarsStatus#104fcfa7 peer:InputPeer = payments.StarsStatus; payments.getStarsTransactions#69da4557 flags:# inbound:flags.0?true outbound:flags.1?true ascending:flags.2?true subscription_id:flags.3?string peer:InputPeer offset:string limit:int = payments.StarsStatus; payments.sendStarsForm#7998c914 form_id:long invoice:InputInvoice = payments.PaymentResult; payments.refundStarsCharge#25ae8f4a user_id:InputUser charge_id:string = Updates; payments.getStarsRevenueStats#d91ffad6 flags:# dark:flags.0?true peer:InputPeer = payments.StarsRevenueStats; payments.getStarsRevenueWithdrawalUrl#13bbe8b3 peer:InputPeer stars:long password:InputCheckPasswordSRP = payments.StarsRevenueWithdrawalUrl; payments.getStarsRevenueAdsAccountUrl#d1d7efc5 peer:InputPeer = payments.StarsRevenueAdsAccountUrl; payments.getStarsTransactionsByID#27842d2e peer:InputPeer id:Vector = payments.StarsStatus; payments.getStarsGiftOptions#d3c96bc8 flags:# user_id:flags.0?InputUser = Vector; payments.getStarsSubscriptions#32512c5 flags:# missing_balance:flags.0?true peer:InputPeer offset:string = payments.StarsStatus; payments.changeStarsSubscription#c7770878 flags:# peer:InputPeer subscription_id:string canceled:flags.0?Bool = Bool; payments.fulfillStarsSubscription#cc5bebb3 peer:InputPeer subscription_id:string = Bool; payments.getStarsGiveawayOptions#bd1efd3e = Vector; payments.getStarGifts#c4563590 hash:int = payments.StarGifts; payments.saveStarGift#2a2a697c flags:# unsave:flags.0?true stargift:InputSavedStarGift = Bool; payments.convertStarGift#74bf076b stargift:InputSavedStarGift = Bool; payments.botCancelStarsSubscription#6dfa0622 flags:# restore:flags.0?true user_id:InputUser charge_id:string = Bool; payments.getConnectedStarRefBots#5869a553 flags:# peer:InputPeer offset_date:flags.2?int offset_link:flags.2?string limit:int = payments.ConnectedStarRefBots; payments.getConnectedStarRefBot#b7d998f0 peer:InputPeer bot:InputUser = payments.ConnectedStarRefBots; payments.getSuggestedStarRefBots#d6b48f7 flags:# order_by_revenue:flags.0?true order_by_date:flags.1?true peer:InputPeer offset:string limit:int = payments.SuggestedStarRefBots; payments.connectStarRefBot#7ed5348a peer:InputPeer bot:InputUser = payments.ConnectedStarRefBots; payments.editConnectedStarRefBot#e4fca4a3 flags:# revoked:flags.0?true peer:InputPeer link:string = payments.ConnectedStarRefBots; payments.getStarGiftUpgradePreview#9c9abcb1 gift_id:long = payments.StarGiftUpgradePreview; payments.upgradeStarGift#aed6e4f5 flags:# keep_original_details:flags.0?true stargift:InputSavedStarGift = Updates; payments.transferStarGift#7f18176a stargift:InputSavedStarGift to_id:InputPeer = Updates; payments.getUniqueStarGift#a1974d72 slug:string = payments.UniqueStarGift; payments.getSavedStarGifts#23830de9 flags:# exclude_unsaved:flags.0?true exclude_saved:flags.1?true exclude_unlimited:flags.2?true exclude_limited:flags.3?true exclude_unique:flags.4?true sort_by_value:flags.5?true peer:InputPeer offset:string limit:int = payments.SavedStarGifts; payments.getSavedStarGift#b455a106 stargift:Vector = payments.SavedStarGifts; payments.getStarGiftWithdrawalUrl#d06e93a8 stargift:InputSavedStarGift password:InputCheckPasswordSRP = payments.StarGiftWithdrawalUrl; payments.toggleChatStarGiftNotifications#60eaefa1 flags:# enabled:flags.0?true peer:InputPeer = Bool; stickers.createStickerSet#9021ab67 flags:# masks:flags.0?true emojis:flags.5?true text_color:flags.6?true user_id:InputUser title:string short_name:string thumb:flags.2?InputDocument stickers:Vector software:flags.3?string = messages.StickerSet; stickers.removeStickerFromSet#f7760f51 sticker:InputDocument = messages.StickerSet; stickers.changeStickerPosition#ffb6d4ca sticker:InputDocument position:int = messages.StickerSet; stickers.addStickerToSet#8653febe stickerset:InputStickerSet sticker:InputStickerSetItem = messages.StickerSet; stickers.setStickerSetThumb#a76a5392 flags:# stickerset:InputStickerSet thumb:flags.0?InputDocument thumb_document_id:flags.1?long = messages.StickerSet; stickers.checkShortName#284b3639 short_name:string = Bool; stickers.suggestShortName#4dafc503 title:string = stickers.SuggestedShortName; stickers.changeSticker#f5537ebc flags:# sticker:InputDocument emoji:flags.0?string mask_coords:flags.1?MaskCoords keywords:flags.2?string = messages.StickerSet; stickers.renameStickerSet#124b1c00 stickerset:InputStickerSet title:string = messages.StickerSet; stickers.deleteStickerSet#87704394 stickerset:InputStickerSet = Bool; stickers.replaceSticker#4696459a sticker:InputDocument new_sticker:InputStickerSetItem = messages.StickerSet; phone.getCallConfig#55451fa9 = DataJSON; phone.requestCall#a6c4600c flags:# video:flags.0?true user_id:InputUser conference_call:flags.1?InputGroupCall random_id:int g_a_hash:bytes protocol:PhoneCallProtocol = phone.PhoneCall; phone.acceptCall#3bd2b4a0 peer:InputPhoneCall g_b:bytes protocol:PhoneCallProtocol = phone.PhoneCall; phone.confirmCall#2efe1722 peer:InputPhoneCall g_a:bytes key_fingerprint:long protocol:PhoneCallProtocol = phone.PhoneCall; phone.receivedCall#17d54f61 peer:InputPhoneCall = Bool; phone.discardCall#b2cbc1c0 flags:# video:flags.0?true peer:InputPhoneCall duration:int reason:PhoneCallDiscardReason connection_id:long = Updates; phone.setCallRating#59ead627 flags:# user_initiative:flags.0?true peer:InputPhoneCall rating:int comment:string = Updates; phone.saveCallDebug#277add7e peer:InputPhoneCall debug:DataJSON = Bool; phone.sendSignalingData#ff7a9383 peer:InputPhoneCall data:bytes = Bool; phone.createGroupCall#48cdc6d8 flags:# rtmp_stream:flags.2?true peer:InputPeer random_id:int title:flags.0?string schedule_date:flags.1?int = Updates; phone.joinGroupCall#d61e1df3 flags:# muted:flags.0?true video_stopped:flags.2?true call:InputGroupCall join_as:InputPeer invite_hash:flags.1?string key_fingerprint:flags.3?long params:DataJSON = Updates; phone.leaveGroupCall#500377f9 call:InputGroupCall source:int = Updates; phone.inviteToGroupCall#7b393160 call:InputGroupCall users:Vector = Updates; phone.discardGroupCall#7a777135 call:InputGroupCall = Updates; phone.toggleGroupCallSettings#74bbb43d flags:# reset_invite_hash:flags.1?true call:InputGroupCall join_muted:flags.0?Bool = Updates; phone.getGroupCall#41845db call:InputGroupCall limit:int = phone.GroupCall; phone.getGroupParticipants#c558d8ab call:InputGroupCall ids:Vector sources:Vector offset:string limit:int = phone.GroupParticipants; phone.checkGroupCall#b59cf977 call:InputGroupCall sources:Vector = Vector; phone.toggleGroupCallRecord#f128c708 flags:# start:flags.0?true video:flags.2?true call:InputGroupCall title:flags.1?string video_portrait:flags.2?Bool = Updates; phone.editGroupCallParticipant#a5273abf flags:# call:InputGroupCall participant:InputPeer muted:flags.0?Bool volume:flags.1?int raise_hand:flags.2?Bool video_stopped:flags.3?Bool video_paused:flags.4?Bool presentation_paused:flags.5?Bool = Updates; phone.editGroupCallTitle#1ca6ac0a call:InputGroupCall title:string = Updates; phone.getGroupCallJoinAs#ef7c213a peer:InputPeer = phone.JoinAsPeers; phone.exportGroupCallInvite#e6aa647f flags:# can_self_unmute:flags.0?true call:InputGroupCall = phone.ExportedGroupCallInvite; phone.toggleGroupCallStartSubscription#219c34e6 call:InputGroupCall subscribed:Bool = Updates; phone.startScheduledGroupCall#5680e342 call:InputGroupCall = Updates; phone.saveDefaultGroupCallJoinAs#575e1f8c peer:InputPeer join_as:InputPeer = Bool; phone.joinGroupCallPresentation#cbea6bc4 call:InputGroupCall params:DataJSON = Updates; phone.leaveGroupCallPresentation#1c50d144 call:InputGroupCall = Updates; phone.getGroupCallStreamChannels#1ab21940 call:InputGroupCall = phone.GroupCallStreamChannels; phone.getGroupCallStreamRtmpUrl#deb3abbf peer:InputPeer revoke:Bool = phone.GroupCallStreamRtmpUrl; phone.saveCallLog#41248786 peer:InputPhoneCall file:InputFile = Bool; phone.createConferenceCall#dfc909ab peer:InputPhoneCall key_fingerprint:long = phone.PhoneCall; langpack.getLangPack#f2f2330a lang_pack:string lang_code:string = LangPackDifference; langpack.getStrings#efea3803 lang_pack:string lang_code:string keys:Vector = Vector; langpack.getDifference#cd984aa5 lang_pack:string lang_code:string from_version:int = LangPackDifference; langpack.getLanguages#42c6978f lang_pack:string = Vector; langpack.getLanguage#6a596502 lang_pack:string lang_code:string = LangPackLanguage; folders.editPeerFolders#6847d0ab folder_peers:Vector = Updates; stats.getBroadcastStats#ab42441a flags:# dark:flags.0?true channel:InputChannel = stats.BroadcastStats; stats.loadAsyncGraph#621d5fa0 flags:# token:string x:flags.0?long = StatsGraph; stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel = stats.MegagroupStats; stats.getMessagePublicForwards#5f150144 channel:InputChannel msg_id:int offset:string limit:int = stats.PublicForwards; stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats; stats.getStoryStats#374fef40 flags:# dark:flags.0?true peer:InputPeer id:int = stats.StoryStats; stats.getStoryPublicForwards#a6437ef6 peer:InputPeer id:int offset:string limit:int = stats.PublicForwards; stats.getBroadcastRevenueStats#f788ee19 flags:# dark:flags.0?true peer:InputPeer = stats.BroadcastRevenueStats; stats.getBroadcastRevenueWithdrawalUrl#9df4faad peer:InputPeer password:InputCheckPasswordSRP = stats.BroadcastRevenueWithdrawalUrl; stats.getBroadcastRevenueTransactions#70990b6d peer:InputPeer offset:int limit:int = stats.BroadcastRevenueTransactions; chatlists.exportChatlistInvite#8472478e chatlist:InputChatlist title:string peers:Vector = chatlists.ExportedChatlistInvite; chatlists.deleteExportedInvite#719c5c5e chatlist:InputChatlist slug:string = Bool; chatlists.editExportedInvite#653db63d flags:# chatlist:InputChatlist slug:string title:flags.1?string peers:flags.2?Vector = ExportedChatlistInvite; chatlists.getExportedInvites#ce03da83 chatlist:InputChatlist = chatlists.ExportedInvites; chatlists.checkChatlistInvite#41c10fff slug:string = chatlists.ChatlistInvite; chatlists.joinChatlistInvite#a6b1e39a slug:string peers:Vector = Updates; chatlists.getChatlistUpdates#89419521 chatlist:InputChatlist = chatlists.ChatlistUpdates; chatlists.joinChatlistUpdates#e089f8f5 chatlist:InputChatlist peers:Vector = Updates; chatlists.hideChatlistUpdates#66e486fb chatlist:InputChatlist = Bool; chatlists.getLeaveChatlistSuggestions#fdbcd714 chatlist:InputChatlist = Vector; chatlists.leaveChatlist#74fae13a chatlist:InputChatlist peers:Vector = Updates; stories.canSendStory#c7dfdfdd peer:InputPeer = Bool; stories.sendStory#e4e6694b flags:# pinned:flags.2?true noforwards:flags.4?true fwd_modified:flags.7?true peer:InputPeer media:InputMedia media_areas:flags.5?Vector caption:flags.0?string entities:flags.1?Vector privacy_rules:Vector random_id:long period:flags.3?int fwd_from_id:flags.6?InputPeer fwd_from_story:flags.6?int = Updates; stories.editStory#b583ba46 flags:# peer:InputPeer id:int media:flags.0?InputMedia media_areas:flags.3?Vector caption:flags.1?string entities:flags.1?Vector privacy_rules:flags.2?Vector = Updates; stories.deleteStories#ae59db5f peer:InputPeer id:Vector = Vector; stories.togglePinned#9a75a1ef peer:InputPeer id:Vector pinned:Bool = Vector; stories.getAllStories#eeb0d625 flags:# next:flags.1?true hidden:flags.2?true state:flags.0?string = stories.AllStories; stories.getPinnedStories#5821a5dc peer:InputPeer offset_id:int limit:int = stories.Stories; stories.getStoriesArchive#b4352016 peer:InputPeer offset_id:int limit:int = stories.Stories; stories.getStoriesByID#5774ca74 peer:InputPeer id:Vector = stories.Stories; stories.toggleAllStoriesHidden#7c2557c4 hidden:Bool = Bool; stories.readStories#a556dac8 peer:InputPeer max_id:int = Vector; stories.incrementStoryViews#b2028afb peer:InputPeer id:Vector = Bool; stories.getStoryViewsList#7ed23c57 flags:# just_contacts:flags.0?true reactions_first:flags.2?true forwards_first:flags.3?true peer:InputPeer q:flags.1?string id:int offset:string limit:int = stories.StoryViewsList; stories.getStoriesViews#28e16cc8 peer:InputPeer id:Vector = stories.StoryViews; stories.exportStoryLink#7b8def20 peer:InputPeer id:int = ExportedStoryLink; stories.report#19d8eb45 peer:InputPeer id:Vector option:bytes message:string = ReportResult; stories.activateStealthMode#57bbd166 flags:# past:flags.0?true future:flags.1?true = Updates; stories.sendReaction#7fd736b2 flags:# add_to_recent:flags.0?true peer:InputPeer story_id:int reaction:Reaction = Updates; stories.getPeerStories#2c4ada50 peer:InputPeer = stories.PeerStories; stories.getAllReadPeerStories#9b5ae7f9 = Updates; stories.getPeerMaxIDs#535983c3 id:Vector = Vector; stories.getChatsToSend#a56a8b60 = messages.Chats; stories.togglePeerStoriesHidden#bd0415c4 peer:InputPeer hidden:Bool = Bool; stories.getStoryReactionsList#b9b2881f flags:# forwards_first:flags.2?true peer:InputPeer id:int reaction:flags.0?Reaction offset:flags.1?string limit:int = stories.StoryReactionsList; stories.togglePinnedToTop#b297e9b peer:InputPeer id:Vector = Bool; stories.searchPosts#d1810907 flags:# hashtag:flags.0?string area:flags.1?MediaArea peer:flags.2?InputPeer offset:string limit:int = stories.FoundStories; premium.getBoostsList#60f67660 flags:# gifts:flags.0?true peer:InputPeer offset:string limit:int = premium.BoostsList; premium.getMyBoosts#be77b4a = premium.MyBoosts; premium.applyBoost#6b7da746 flags:# slots:flags.0?Vector peer:InputPeer = premium.MyBoosts; premium.getBoostsStatus#42f1f61 peer:InputPeer = premium.BoostsStatus; premium.getUserBoosts#39854d1f peer:InputPeer user_id:InputUser = premium.BoostsList; smsjobs.isEligibleToJoin#edc39d0 = smsjobs.EligibilityToJoin; smsjobs.join#a74ece2d = Bool; smsjobs.leave#9898ad73 = Bool; smsjobs.updateSettings#93fa0bf flags:# allow_international:flags.0?true = Bool; smsjobs.getStatus#10a698e8 = smsjobs.Status; smsjobs.getSmsJob#778d902f job_id:string = SmsJob; smsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool; fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo; // LAYER 199 Telethon-1.39.0/telethon_generator/data/errors.csv000066400000000000000000001154131475566265000222450ustar00rootroot00000000000000name,codes,description 2FA_CONFIRM_WAIT_X,420,The account is 2FA protected so it will be deleted in a week. Otherwise it can be reset in {seconds} ABOUT_TOO_LONG,400,The provided bio is too long ACCESS_TOKEN_EXPIRED,400,Bot token expired ACCESS_TOKEN_INVALID,400,The provided token is not valid ACTIVE_USER_REQUIRED,401,The method is only available to already activated users ADMINS_TOO_MUCH,400,Too many admins ADMIN_ID_INVALID,400,The specified admin ID is invalid ADMIN_RANK_EMOJI_NOT_ALLOWED,400,Emoji are not allowed in admin titles or ranks ADMIN_RANK_INVALID,400,The given admin title or rank was invalid (possibly larger than 16 characters) ALBUM_PHOTOS_TOO_MANY,400,Too many photos were included in the album API_ID_INVALID,400,The api_id/api_hash combination is invalid API_ID_PUBLISHED_FLOOD,400,"This API id was published somewhere, you can't use it now" ARTICLE_TITLE_EMPTY,400,The title of the article is empty AUDIO_CONTENT_URL_EMPTY,400,The remote URL specified in the content field is empty AUDIO_TITLE_EMPTY,400,The title attribute of the audio must be non-empty AUTH_BYTES_INVALID,400,The provided authorization is invalid AUTH_KEY_DUPLICATED,406,"The authorization key (session file) was used under two different IP addresses simultaneously, and can no longer be used. Use the same session exclusively, or use different sessions" AUTH_KEY_INVALID,401,The key is invalid AUTH_KEY_PERM_EMPTY,401,"The method is unavailable for temporary authorization key, not bound to permanent" AUTH_KEY_UNREGISTERED,401,The key is not registered in the system AUTH_RESTART,500,Restart the authorization process AUTH_TOKEN_ALREADY_ACCEPTED,400,The authorization token was already used AUTH_TOKEN_EXCEPTION,400,An error occurred while importing the auth token AUTH_TOKEN_EXPIRED,400,The provided authorization token has expired and the updated QR-code must be re-scanned AUTH_TOKEN_INVALID,400,An invalid authorization token was provided AUTH_TOKEN_INVALID2,400,An invalid authorization token was provided AUTH_TOKEN_INVALIDX,400,The specified auth token is invalid AUTOARCHIVE_NOT_AVAILABLE,400,You cannot use this feature yet BANK_CARD_NUMBER_INVALID,400,Incorrect credit card number BANNED_RIGHTS_INVALID,400,"You cannot use that set of permissions in this request, i.e. restricting view_messages as a default" BASE_PORT_LOC_INVALID,400,Base port location invalid BOTS_TOO_MUCH,400,There are too many bots in this chat/channel BOT_CHANNELS_NA,400,Bots can't edit admin privileges BOT_COMMAND_DESCRIPTION_INVALID,400,"The command description was empty, too long or had invalid characters used" BOT_COMMAND_INVALID,400,The specified command is invalid BOT_DOMAIN_INVALID,400,The domain used for the auth button does not match the one configured in @BotFather BOT_GAMES_DISABLED,400,Bot games cannot be used in this type of chat BOT_GROUPS_BLOCKED,400,This bot can't be added to groups BOT_INLINE_DISABLED,400,This bot can't be used in inline mode BOT_INVALID,400,This is not a valid bot BOT_METHOD_INVALID,400,The API access for bot users is restricted. The method you tried to invoke cannot be executed as a bot BOT_MISSING,400,This method can only be run by a bot BOT_ONESIDE_NOT_AVAIL,400,Bots can't pin messages in PM just for themselves BOT_PAYMENTS_DISABLED,400,This method can only be run by a bot BOT_POLLS_DISABLED,400,You cannot create polls under a bot account BOT_RESPONSE_TIMEOUT,400,The bot did not answer to the callback query in time BOT_SCORE_NOT_MODIFIED,400,The score wasn't modified BROADCAST_CALLS_DISABLED,400, BROADCAST_FORBIDDEN,403,The request cannot be used in broadcast channels BROADCAST_ID_INVALID,400,The channel is invalid BROADCAST_PUBLIC_VOTERS_FORBIDDEN,400,You cannot broadcast polls where the voters are public BROADCAST_REQUIRED,400,The request can only be used with a broadcast channel BUTTON_DATA_INVALID,400,The provided button data is invalid BUTTON_TEXT_INVALID,400,The specified button text is invalid BUTTON_TYPE_INVALID,400,The type of one of the buttons you provided is invalid BUTTON_URL_INVALID,400,Button URL invalid BUTTON_USER_PRIVACY_RESTRICTED,400,The privacy setting of the user specified in a [inputKeyboardButtonUserProfile](/constructor/inputKeyboardButtonUserProfile) button do not allow creating such a button CALL_ALREADY_ACCEPTED,400,The call was already accepted CALL_ALREADY_DECLINED,400,The call was already declined CALL_OCCUPY_FAILED,500,The call failed because the user is already making another call CALL_PEER_INVALID,400,The provided call peer object is invalid CALL_PROTOCOL_FLAGS_INVALID,400,Call protocol flags invalid CDN_METHOD_INVALID,400,This method cannot be invoked on a CDN server. Refer to https://core.telegram.org/cdn#schema for available methods CDN_UPLOAD_TIMEOUT,500,A server-side timeout occurred while reuploading the file to the CDN DC CHANNELS_ADMIN_LOCATED_TOO_MUCH,400,The user has reached the limit of public geogroups CHANNELS_ADMIN_PUBLIC_TOO_MUCH,400,"You're admin of too many public channels, make some channels private to change the username of this channel" CHANNELS_TOO_MUCH,400,You have joined too many channels/supergroups CHANNEL_BANNED,400,The channel is banned CHANNEL_FORUM_MISSING,400, CHANNEL_ID_INVALID,400,The specified supergroup ID is invalid CHANNEL_INVALID,400,"Invalid channel object. Make sure to pass the right types, for instance making sure that the request is designed for channels or otherwise look for a different one more suited" CHANNEL_PARICIPANT_MISSING,400,The current user is not in the channel CHANNEL_PRIVATE,400 406,The channel specified is private and you lack permission to access it. Another reason may be that you were banned from it CHANNEL_PUBLIC_GROUP_NA,403,channel/supergroup not available CHANNEL_TOO_BIG,400, CHANNEL_TOO_LARGE,400 406,Channel is too large to be deleted; this error is issued when trying to delete channels with more than 1000 members (subject to change) CHAT_ABOUT_NOT_MODIFIED,400,About text has not changed CHAT_ABOUT_TOO_LONG,400,Chat about too long CHAT_ADMIN_INVITE_REQUIRED,403,You do not have the rights to do this CHAT_ADMIN_REQUIRED,400 403,"Chat admin privileges are required to do that in the specified chat (for example, to send a message in a channel which is not yours), or invalid permissions used for the channel or group" CHAT_DISCUSSION_UNALLOWED,400, CHAT_FORBIDDEN,403,You cannot write in this chat CHAT_FORWARDS_RESTRICTED,400 406,You can't forward messages from a protected chat CHAT_GET_FAILED,500, CHAT_GUEST_SEND_FORBIDDEN,403,"You join the discussion group before commenting, see [here](/api/discussion#requiring-users-to-join-the-group) for more info" CHAT_ID_EMPTY,400,The provided chat ID is empty CHAT_ID_GENERATE_FAILED,500,Failure while generating the chat ID CHAT_ID_INVALID,400,"Invalid object ID for a chat. Make sure to pass the right types, for instance making sure that the request is designed for chats (not channels/megagroups) or otherwise look for a different one more suited\nAn example working with a megagroup and AddChatUserRequest, it will fail because megagroups are channels. Use InviteToChannelRequest instead" CHAT_INVALID,400,The chat is invalid for this request CHAT_INVITE_PERMANENT,400,You can't set an expiration date on permanent invite links CHAT_LINK_EXISTS,400,The chat is linked to a channel and cannot be used in that request CHAT_NOT_MODIFIED,400,"The chat or channel wasn't modified (title, invites, username, admins, etc. are the same)" CHAT_RESTRICTED,400,The chat is restricted and cannot be used in that request CHAT_REVOKE_DATE_UNSUPPORTED,400,`min_date` and `max_date` are not available for using with non-user peers CHAT_SEND_GAME_FORBIDDEN,403,You can't send a game to this chat CHAT_SEND_GIFS_FORBIDDEN,403,You can't send gifs in this chat CHAT_SEND_INLINE_FORBIDDEN,400 403,You cannot send inline results in this chat CHAT_SEND_MEDIA_FORBIDDEN,403,You can't send media in this chat CHAT_SEND_POLL_FORBIDDEN,403,You can't send polls in this chat CHAT_SEND_STICKERS_FORBIDDEN,403,You can't send stickers in this chat CHAT_TITLE_EMPTY,400,No chat title provided CHAT_TOO_BIG,400,"This method is not available for groups with more than `chat_read_mark_size_threshold` members, [see client configuration](https://core.telegram.org/api/config#client-configuration)" CHAT_WRITE_FORBIDDEN,403,You can't write in this chat CHP_CALL_FAIL,500,The statistics cannot be retrieved at this time CODE_EMPTY,400,The provided code is empty CODE_HASH_INVALID,400,Code hash invalid CODE_INVALID,400,Code invalid (i.e. from email) CONNECTION_API_ID_INVALID,400,The provided API id is invalid CONNECTION_APP_VERSION_EMPTY,400,App version is empty CONNECTION_DEVICE_MODEL_EMPTY,400,Device model empty CONNECTION_LANG_PACK_INVALID,400,"The specified language pack is not valid. This is meant to be used by official applications only so far, leave it empty" CONNECTION_LAYER_INVALID,400,The very first request must always be InvokeWithLayerRequest CONNECTION_NOT_INITED,400,Connection not initialized CONNECTION_SYSTEM_EMPTY,400,Connection system empty CONNECTION_SYSTEM_LANG_CODE_EMPTY,400,The system language string was empty during connection CONTACT_ADD_MISSING,400,Contact to add is missing CONTACT_ID_INVALID,400,The provided contact ID is invalid CONTACT_NAME_EMPTY,400,The provided contact name cannot be empty CONTACT_REQ_MISSING,400,Missing contact request CREATE_CALL_FAILED,400,An error occurred while creating the call CURRENCY_TOTAL_AMOUNT_INVALID,400,The total amount of all prices is invalid DATA_INVALID,400,Encrypted data invalid DATA_JSON_INVALID,400,The provided JSON data is invalid DATA_TOO_LONG,400,Data too long DATE_EMPTY,400,Date empty DC_ID_INVALID,400,This occurs when an authorization is tried to be exported for the same data center one is currently connected to DH_G_A_INVALID,400,g_a invalid DOCUMENT_INVALID,400,The document file was invalid and can't be used in inline mode EDIT_BOT_INVITE_FORBIDDEN,403,Normal users can't edit invites that were created by bots EMAIL_HASH_EXPIRED,400,The email hash expired and cannot be used to verify it EMAIL_INVALID,400,The given email is invalid EMAIL_UNCONFIRMED,400,Email unconfirmed EMAIL_UNCONFIRMED_X,400,"Email unconfirmed, the length of the code must be {code_length}" EMAIL_VERIFY_EXPIRED,400,The verification email has expired EMOJI_INVALID,400,The specified theme emoji is valid EMOJI_NOT_MODIFIED,400,The theme wasn't changed EMOTICON_EMPTY,400,The emoticon field cannot be empty EMOTICON_INVALID,400,The specified emoticon cannot be used or was not a emoticon EMOTICON_STICKERPACK_MISSING,400,The emoticon sticker pack you are trying to get is missing ENCRYPTED_MESSAGE_INVALID,400,Encrypted message invalid ENCRYPTION_ALREADY_ACCEPTED,400,Secret chat already accepted ENCRYPTION_ALREADY_DECLINED,400,The secret chat was already declined ENCRYPTION_DECLINED,400,The secret chat was declined ENCRYPTION_ID_INVALID,400,The provided secret chat ID is invalid ENCRYPTION_OCCUPY_FAILED,500,TDLib developer claimed it is not an error while accepting secret chats and 500 is used instead of 420 ENTITIES_TOO_LONG,400,It is no longer possible to send such long data inside entity tags (for example inline text URLs) ENTITY_BOUNDS_INVALID,400,Some of provided entities have invalid bounds (length is zero or out of the boundaries of the string) ENTITY_MENTION_USER_INVALID,400,You can't use this entity ERROR_TEXT_EMPTY,400,The provided error message is empty EXPIRE_DATE_INVALID,400,The specified expiration date is invalid EXPIRE_FORBIDDEN,400, EXPORT_CARD_INVALID,400,Provided card is invalid EXTERNAL_URL_INVALID,400,External URL invalid FIELD_NAME_EMPTY,400,The field with the name FIELD_NAME is missing FIELD_NAME_INVALID,400,The field with the name FIELD_NAME is invalid FILEREF_UPGRADE_NEEDED,406,The file reference needs to be refreshed before being used again FILE_CONTENT_TYPE_INVALID,400,File content-type is invalid FILE_EMTPY,400,An empty file was provided FILE_ID_INVALID,400,"The provided file id is invalid. Make sure all parameters are present, have the correct type and are not empty (ID, access hash, file reference, thumb size ...)" FILE_MIGRATE_X,303,The file to be accessed is currently stored in DC {new_dc} FILE_PARTS_INVALID,400,The number of file parts is invalid FILE_PART_0_MISSING,400,File part 0 missing FILE_PART_EMPTY,400,The provided file part is empty FILE_PART_INVALID,400,The file part number is invalid FILE_PART_LENGTH_INVALID,400,The length of a file part is invalid FILE_PART_SIZE_CHANGED,400,The file part size (chunk size) cannot change during upload FILE_PART_SIZE_INVALID,400,The provided file part size is invalid FILE_PART_TOO_BIG,400,The uploaded file part is too big FILE_PART_X_MISSING,400,Part {which} of the file is missing from storage FILE_REFERENCE_EMPTY,400,The file reference must exist to access the media and it cannot be empty FILE_REFERENCE_EXPIRED,400,The file reference has expired and is no longer valid or it belongs to self-destructing media and cannot be resent FILE_REFERENCE_INVALID,400,The file reference is invalid or you can't do that operation on such message FILE_TITLE_EMPTY,400,An empty file title was specified FILTER_ID_INVALID,400,The specified filter ID is invalid FILTER_INCLUDE_EMPTY,400,The include_peers vector of the filter is empty FILTER_NOT_SUPPORTED,400,The specified filter cannot be used in this context FILTER_TITLE_EMPTY,400,The title field of the filter is empty FIRSTNAME_INVALID,400,The first name is invalid FLOOD_TEST_PHONE_WAIT_X,420,A wait of {seconds} seconds is required in the test servers FLOOD_WAIT_X,420,A wait of {seconds} seconds is required FLOOD_PREMIUM_WAIT_X,420,A wait of {seconds} seconds is required in non-premium accounts FOLDER_ID_EMPTY,400,The folder you tried to delete was already empty FOLDER_ID_INVALID,400,The folder you tried to use was not valid FRESH_CHANGE_ADMINS_FORBIDDEN,400 406,Recently logged-in users cannot add or change admins FRESH_CHANGE_PHONE_FORBIDDEN,406,Recently logged-in users cannot use this request FRESH_RESET_AUTHORISATION_FORBIDDEN,406,The current session is too new and cannot be used to reset other authorisations yet FROM_MESSAGE_BOT_DISABLED,400,Bots can't use fromMessage min constructors FROM_PEER_INVALID,400,The given from_user peer cannot be used for the parameter GAME_BOT_INVALID,400,You cannot send that game with the current bot GEO_POINT_INVALID,400,Invalid geoposition provided GIF_CONTENT_TYPE_INVALID,400,GIF content-type invalid GIF_ID_INVALID,400,The provided GIF ID is invalid GRAPH_EXPIRED_RELOAD,400,"This graph has expired, please obtain a new graph token" GRAPH_INVALID_RELOAD,400,"Invalid graph token provided, please reload the stats and provide the updated token" GRAPH_OUTDATED_RELOAD,400,"Data can't be used for the channel statistics, graphs outdated" GROUPCALL_ADD_PARTICIPANTS_FAILED,500, GROUPCALL_ALREADY_DISCARDED,400,The group call was already discarded GROUPCALL_ALREADY_STARTED,403,"The groupcall has already started, you can join directly using [phone.joinGroupCall](https://core.telegram.org/method/phone.joinGroupCall)" GROUPCALL_FORBIDDEN,403,The group call has already ended GROUPCALL_INVALID,400,The specified group call is invalid GROUPCALL_JOIN_MISSING,400,You haven't joined this group call GROUPCALL_NOT_MODIFIED,400,Group call settings weren't modified GROUPCALL_SSRC_DUPLICATE_MUCH,400,The app needs to retry joining the group call with a new SSRC value GROUPED_MEDIA_INVALID,400,Invalid grouped media GROUP_CALL_INVALID,400,Group call invalid HASH_INVALID,400,The provided hash is invalid HIDE_REQUESTER_MISSING,400,The join request was missing or was already handled HISTORY_GET_FAILED,500,Fetching of history failed IMAGE_PROCESS_FAILED,400,Failure while processing image IMPORT_FILE_INVALID,400,The file is too large to be imported IMPORT_FORMAT_UNRECOGNIZED,400,Unknown import format IMPORT_ID_INVALID,400,The specified import ID is invalid INLINE_BOT_REQUIRED,403,The action must be performed through an inline bot callback INLINE_RESULT_EXPIRED,400,The inline query expired INPUT_CONSTRUCTOR_INVALID,400,The provided constructor is invalid INPUT_FETCH_ERROR,400,An error occurred while deserializing TL parameters INPUT_FETCH_FAIL,400,Failed deserializing TL payload INPUT_FILTER_INVALID,400,The search query filter is invalid INPUT_LAYER_INVALID,400,The provided layer is invalid INPUT_METHOD_INVALID,400,The invoked method does not exist anymore or has never existed INPUT_REQUEST_TOO_LONG,400,The input request was too long. This may be a bug in the library as it can occur when serializing more bytes than it should (like appending the vector constructor code at the end of a message) INPUT_TEXT_EMPTY,400,The specified text is empty INPUT_USER_DEACTIVATED,400,The specified user was deleted INTERDC_X_CALL_ERROR,500,An error occurred while communicating with DC {dc} INTERDC_X_CALL_RICH_ERROR,500,A rich error occurred while communicating with DC {dc} INVITE_FORBIDDEN_WITH_JOINAS,400,"If the user has anonymously joined a group call as a channel, they can't invite other users to the group call because that would cause deanonymization, because the invite would be sent using the original user ID, not the anonymized channel ID" INVITE_HASH_EMPTY,400,The invite hash is empty INVITE_HASH_EXPIRED,400 406,The chat the user tried to join has expired and is not valid anymore INVITE_HASH_INVALID,400,The invite hash is invalid INVITE_REQUEST_SENT,400,You have successfully requested to join this chat or channel INVITE_REVOKED_MISSING,400,The specified invite link was already revoked or is invalid INVOICE_PAYLOAD_INVALID,400,The specified invoice payload is invalid JOIN_AS_PEER_INVALID,400,The specified peer cannot be used to join a group call LANG_CODE_INVALID,400,The specified language code is invalid LANG_CODE_NOT_SUPPORTED,400,The specified language code is not supported LANG_PACK_INVALID,400,The provided language pack is invalid LASTNAME_INVALID,400,The last name is invalid LIMIT_INVALID,400,An invalid limit was provided. See https://core.telegram.org/api/files#downloading-files LINK_NOT_MODIFIED,400,The channel is already linked to this group LOCATION_INVALID,400,The location given for a file was invalid. See https://core.telegram.org/api/files#downloading-files MAX_DATE_INVALID,400,The specified maximum date is invalid MAX_ID_INVALID,400,The provided max ID is invalid MAX_QTS_INVALID,400,The provided QTS were invalid MD5_CHECKSUM_INVALID,400,The MD5 check-sums do not match MEDIA_CAPTION_TOO_LONG,400,The caption is too long MEDIA_EMPTY,400,The provided media object is invalid or the current account may not be able to send it (such as games as users) MEDIA_GROUPED_INVALID,400,You tried to send media of different types in an album MEDIA_INVALID,400,Media invalid MEDIA_NEW_INVALID,400,The new media to edit the message with is invalid (such as stickers or voice notes) MEDIA_PREV_INVALID,400,The old media cannot be edited with anything else (such as stickers or voice notes) MEDIA_TTL_INVALID,400, MEGAGROUP_ID_INVALID,400,The group is invalid MEGAGROUP_PREHISTORY_HIDDEN,400,You can't set this discussion group because it's history is hidden MEGAGROUP_REQUIRED,400,The request can only be used with a megagroup channel MEMBER_NO_LOCATION,500,An internal failure occurred while fetching user info (couldn't find location) MEMBER_OCCUPY_PRIMARY_LOC_FAILED,500,Occupation of primary member location failed MESSAGE_AUTHOR_REQUIRED,403,Message author required MESSAGE_DELETE_FORBIDDEN,403,"You can't delete one of the messages you tried to delete, most likely because it is a service message." MESSAGE_EDIT_TIME_EXPIRED,400,"You can't edit this message anymore, too much time has passed since its creation." MESSAGE_EMPTY,400,Empty or invalid UTF-8 message was sent MESSAGE_IDS_EMPTY,400,No message ids were provided MESSAGE_ID_INVALID,400,The specified message ID is invalid or you can't do that operation on such message MESSAGE_NOT_MODIFIED,400,Content of the message was not modified MESSAGE_POLL_CLOSED,400,The poll was closed and can no longer be voted on MESSAGE_TOO_LONG,400,Message was too long METHOD_INVALID,400,The API method is invalid and cannot be used MIN_DATE_INVALID,400,The specified minimum date is invalid MSGID_DECREASE_RETRY,500,The request should be retried with a lower message ID MSG_ID_INVALID,400,The message ID used in the peer was invalid MSG_TOO_OLD,400,"[`chat_read_mark_expire_period` seconds](https://core.telegram.org/api/config#chat-read-mark-expire-period) have passed since the message was sent, read receipts were deleted" MSG_WAIT_FAILED,400,A waiting call returned an error MT_SEND_QUEUE_TOO_LONG,500, MULTI_MEDIA_TOO_LONG,400,Too many media files were included in the same album NEED_CHAT_INVALID,500,The provided chat is invalid NEED_MEMBER_INVALID,500,The provided member is invalid or does not exist (for example a thumb size) NETWORK_MIGRATE_X,303,The source IP address is associated with DC {new_dc} NEW_SALT_INVALID,400,The new salt is invalid NEW_SETTINGS_EMPTY,400,"No password is set on the current account, and no new password was specified in `new_settings`" NEW_SETTINGS_INVALID,400,The new settings are invalid NEXT_OFFSET_INVALID,400,The value for next_offset is invalid. Check that it has normal characters and is not too long NOT_ALLOWED,403, OFFSET_INVALID,400,"The given offset was invalid, it must be divisible by 1KB. See https://core.telegram.org/api/files#downloading-files" OFFSET_PEER_ID_INVALID,400,The provided offset peer is invalid OPTIONS_TOO_MUCH,400,You defined too many options for the poll OPTION_INVALID,400,The option specified is invalid and does not exist in the target poll PACK_SHORT_NAME_INVALID,400,"Invalid sticker pack name. It must begin with a letter, can't contain consecutive underscores and must end in ""_by_""." PACK_SHORT_NAME_OCCUPIED,400,A stickerpack with this name already exists PACK_TITLE_INVALID,400,The stickerpack title is invalid PARTICIPANTS_TOO_FEW,400,Not enough participants PARTICIPANT_CALL_FAILED,500,Failure while making call PARTICIPANT_ID_INVALID,400,The specified participant ID is invalid PARTICIPANT_JOIN_MISSING,400 403,"Trying to enable a presentation, when the user hasn't joined the Video Chat with [phone.joinGroupCall](https://core.telegram.org/method/phone.joinGroupCall)" PARTICIPANT_VERSION_OUTDATED,400,The other participant does not use an up to date telegram client with support for calls PASSWORD_EMPTY,400,The provided password is empty PASSWORD_HASH_INVALID,400,The password (and thus its hash value) you entered is invalid PASSWORD_MISSING,400,The account must have 2-factor authentication enabled (a password) before this method can be used PASSWORD_RECOVERY_EXPIRED,400,The recovery code has expired PASSWORD_RECOVERY_NA,400,"No email was set, can't recover password via email" PASSWORD_REQUIRED,400,The account must have 2-factor authentication enabled (a password) before this method can be used PASSWORD_TOO_FRESH_X,400,The password was added too recently and {seconds} seconds must pass before using the method PAYMENT_PROVIDER_INVALID,400,The payment provider was not recognised or its token was invalid PEER_FLOOD,400,Too many requests PEER_HISTORY_EMPTY,400, PEER_ID_INVALID,400,"An invalid Peer was used. Make sure to pass the right peer type and that the value is valid (for instance, bots cannot start conversations)" PEER_ID_NOT_SUPPORTED,400,The provided peer ID is not supported PERSISTENT_TIMESTAMP_EMPTY,400,Persistent timestamp empty PERSISTENT_TIMESTAMP_INVALID,400,Persistent timestamp invalid PERSISTENT_TIMESTAMP_OUTDATED,500,Persistent timestamp outdated PHONE_CODE_EMPTY,400,The phone code is missing PHONE_CODE_EXPIRED,400,The confirmation code has expired PHONE_CODE_HASH_EMPTY,400,The phone code hash is missing PHONE_CODE_INVALID,400,The phone code entered was invalid PHONE_HASH_EXPIRED,400,An invalid or expired `phone_code_hash` was provided PHONE_MIGRATE_X,303,The phone number a user is trying to use for authorization is associated with DC {new_dc} PHONE_NOT_OCCUPIED,400,No user is associated to the specified phone number PHONE_NUMBER_APP_SIGNUP_FORBIDDEN,400,You can't sign up using this app PHONE_NUMBER_BANNED,400,The used phone number has been banned from Telegram and cannot be used anymore. Maybe check https://www.telegram.org/faq_spam PHONE_NUMBER_FLOOD,400,You asked for the code too many times. PHONE_NUMBER_INVALID,400 406,The phone number is invalid PHONE_NUMBER_OCCUPIED,400,The phone number is already in use PHONE_NUMBER_UNOCCUPIED,400,The phone number is not yet being used PHONE_PASSWORD_FLOOD,406,You have tried logging in too many times PHONE_PASSWORD_PROTECTED,400,This phone is password protected PHOTO_CONTENT_TYPE_INVALID,400,Photo mime-type invalid PHOTO_CONTENT_URL_EMPTY,400,The content from the URL used as a photo appears to be empty or has caused another HTTP error PHOTO_CROP_FILE_MISSING,400,Photo crop file missing PHOTO_CROP_SIZE_SMALL,400,Photo is too small PHOTO_EXT_INVALID,400,The extension of the photo is invalid PHOTO_FILE_MISSING,400,Profile photo file missing PHOTO_ID_INVALID,400,Photo id is invalid PHOTO_INVALID,400,Photo invalid PHOTO_INVALID_DIMENSIONS,400,The photo dimensions are invalid (hint: `pip install pillow` for `send_file` to resize images) PHOTO_SAVE_FILE_INVALID,400,The photo you tried to send cannot be saved by Telegram. A reason may be that it exceeds 10MB. Try resizing it locally PHOTO_THUMB_URL_EMPTY,400,The URL used as a thumbnail appears to be empty or has caused another HTTP error PINNED_DIALOGS_TOO_MUCH,400,Too many pinned dialogs PIN_RESTRICTED,400,You can't pin messages in private chats with other people POLL_ANSWERS_INVALID,400,The poll did not have enough answers or had too many POLL_ANSWER_INVALID,400,One of the poll answers is not acceptable POLL_OPTION_DUPLICATE,400,A duplicate option was sent in the same poll POLL_OPTION_INVALID,400,A poll option used invalid data (the data may be too long) POLL_QUESTION_INVALID,400,The poll question was either empty or too long POLL_UNSUPPORTED,400,This layer does not support polls in the issued method POLL_VOTE_REQUIRED,403,Cast a vote in the poll before calling this method POSTPONED_TIMEOUT,500,The postponed call has timed out PREMIUM_ACCOUNT_REQUIRED,403,A premium account is required to execute this action PREMIUM_CURRENTLY_UNAVAILABLE,406, PREVIOUS_CHAT_IMPORT_ACTIVE_WAIT_XMIN,406,"Similar to a flood wait, must wait {minutes} minutes" PRIVACY_KEY_INVALID,400,The privacy key is invalid PRIVACY_TOO_LONG,400,Cannot add that many entities in a single request PRIVACY_VALUE_INVALID,400,The privacy value is invalid PTS_CHANGE_EMPTY,500,No PTS change PUBLIC_CHANNEL_MISSING,403,You can only export group call invite links for public chats or channels PUBLIC_KEY_REQUIRED,400,A public key is required QUERY_ID_EMPTY,400,The query ID is empty QUERY_ID_INVALID,400,The query ID is invalid QUERY_TOO_SHORT,400,The query string is too short QUIZ_ANSWER_MISSING,400,You can forward a quiz while hiding the original author only after choosing an option in the quiz QUIZ_CORRECT_ANSWERS_EMPTY,400,A quiz must specify one correct answer QUIZ_CORRECT_ANSWERS_TOO_MUCH,400,There can only be one correct answer QUIZ_CORRECT_ANSWER_INVALID,400,The correct answer is not an existing answer QUIZ_MULTIPLE_INVALID,400,A poll cannot be both multiple choice and quiz RANDOM_ID_DUPLICATE,500,You provided a random ID that was already used RANDOM_ID_EMPTY,400,Random ID empty RANDOM_ID_INVALID,400,A provided random ID is invalid RANDOM_LENGTH_INVALID,400,Random length invalid RANGES_INVALID,400,Invalid range provided REACTIONS_TOO_MANY,400,"The message already has exactly `reactions_uniq_max` reaction emojis, you can't react with a new emoji, see [the docs for more info](/api/config#client-configuration)" REACTION_EMPTY,400,No reaction provided REACTION_INVALID,400,Invalid reaction provided (only emoji are allowed) REFLECTOR_NOT_AVAILABLE,400,Invalid call reflector server REG_ID_GENERATE_FAILED,500,Failure while generating registration ID REPLY_MARKUP_BUY_EMPTY,400,Reply markup for buy button empty REPLY_MARKUP_GAME_EMPTY,400,The provided reply markup for the game is empty REPLY_MARKUP_INVALID,400,The provided reply markup is invalid REPLY_MARKUP_TOO_LONG,400,The data embedded in the reply markup buttons was too much RESET_REQUEST_MISSING,400,No password reset is in progress RESULTS_TOO_MUCH,400,"You sent too many results, see https://core.telegram.org/bots/api#answerinlinequery for the current limit" RESULT_ID_DUPLICATE,400,Duplicated IDs on the sent results. Make sure to use unique IDs RESULT_ID_EMPTY,400,Result ID empty RESULT_ID_INVALID,400,The given result cannot be used to send the selection to the bot RESULT_TYPE_INVALID,400,Result type invalid REVOTE_NOT_ALLOWED,400,You cannot change your vote RIGHTS_NOT_MODIFIED,400,"The new admin rights are equal to the old rights, no change was made" RIGHT_FORBIDDEN,403,Either your admin rights do not allow you to do this or you passed the wrong rights combination (some rights only apply to channels and vice versa) RPC_CALL_FAIL,500,"Telegram is having internal issues, please try again later." RPC_MCGET_FAIL,500,"Telegram is having internal issues, please try again later." RSA_DECRYPT_FAILED,400,Internal RSA decryption failed SCHEDULE_BOT_NOT_ALLOWED,400,Bots are not allowed to schedule messages SCHEDULE_DATE_INVALID,400,Invalid schedule date provided SCHEDULE_DATE_TOO_LATE,400,The date you tried to schedule is too far in the future (last known limit of 1 year and a few hours) SCHEDULE_STATUS_PRIVATE,400,You cannot schedule a message until the person comes online if their privacy does not show this information SCHEDULE_TOO_MUCH,400,You cannot schedule more messages in this chat (last known limit of 100 per chat) SCORE_INVALID,400,The specified game score is invalid SEARCH_QUERY_EMPTY,400,The search query is empty SEARCH_WITH_LINK_NOT_SUPPORTED,400,You cannot provide a search query and an invite link at the same time SECONDS_INVALID,400,"Slow mode only supports certain values (e.g. 0, 10s, 30s, 1m, 5m, 15m and 1h)" SEND_AS_PEER_INVALID,400,You can't send messages as the specified peer SEND_CODE_UNAVAILABLE,406,"Returned when all available options for this type of number were already used (e.g. flash-call, then SMS, then this error might be returned to trigger a second resend)" SEND_MESSAGE_MEDIA_INVALID,400,The message media was invalid or not specified SEND_MESSAGE_TYPE_INVALID,400,The message type is invalid SENSITIVE_CHANGE_FORBIDDEN,403,Your sensitive content settings cannot be changed at this time SESSION_EXPIRED,401,The authorization has expired SESSION_PASSWORD_NEEDED,401,Two-steps verification is enabled and a password is required SESSION_REVOKED,401,"The authorization has been invalidated, because of the user terminating all sessions" SESSION_TOO_FRESH_X,400,The session logged in too recently and {seconds} seconds must pass before calling the method SETTINGS_INVALID,400,Invalid settings were provided SHA256_HASH_INVALID,400,The provided SHA256 hash is invalid SHORTNAME_OCCUPY_FAILED,400,An error occurred when trying to register the short-name used for the sticker pack. Try a different name SHORT_NAME_INVALID,400,The specified short name is invalid SHORT_NAME_OCCUPIED,400,The specified short name is already in use SIGN_IN_FAILED,500,Failure while signing in SLOWMODE_MULTI_MSGS_DISABLED,400,"Slowmode is enabled, you cannot forward multiple messages to this group" SLOWMODE_WAIT_X,420,A wait of {seconds} seconds is required before sending another message in this chat SMS_CODE_CREATE_FAILED,400,An error occurred while creating the SMS code SRP_ID_INVALID,400,Invalid SRP ID provided SRP_PASSWORD_CHANGED,400,Password has changed START_PARAM_EMPTY,400,The start parameter is empty START_PARAM_INVALID,400,Start parameter invalid START_PARAM_TOO_LONG,400,Start parameter is too long STATS_MIGRATE_X,303,The channel statistics must be fetched from DC {dc} STICKERPACK_STICKERS_TOO_MUCH,400,"There are too many stickers in this stickerpack, you can't add any more" STICKERSET_INVALID,400 406,The provided sticker set is invalid STICKERSET_OWNER_ANONYMOUS,406,This sticker set can't be used as the group's official stickers because it was created by one of its anonymous admins STICKERS_EMPTY,400,No sticker provided STICKERS_TOO_MUCH,400,"There are too many stickers in this stickerpack, you can't add any more" STICKER_DOCUMENT_INVALID,400,"The sticker file was invalid (this file has failed Telegram internal checks, make sure to use the correct format and comply with https://core.telegram.org/animated_stickers)" STICKER_EMOJI_INVALID,400,Sticker emoji invalid STICKER_FILE_INVALID,400,Sticker file invalid STICKER_GIF_DIMENSIONS,400,The specified video sticker has invalid dimensions STICKER_ID_INVALID,400,The provided sticker ID is invalid STICKER_INVALID,400,The provided sticker is invalid STICKER_MIME_INVALID,400,Make sure to pass a valid image file for the right InputFile parameter STICKER_PNG_DIMENSIONS,400,Sticker png dimensions invalid STICKER_PNG_NOPNG,400,Stickers must be a png file but the used image was not a png STICKER_TGS_NODOC,400,You must send the animated sticker as a document STICKER_TGS_NOTGS,400,Stickers must be a tgs file but the used file was not a tgs STICKER_THUMB_PNG_NOPNG,400,Stickerset thumb must be a png file but the used file was not png STICKER_THUMB_TGS_NOTGS,400,Stickerset thumb must be a tgs file but the used file was not tgs STICKER_VIDEO_BIG,400,The specified video sticker is too big STICKER_VIDEO_NODOC,400,You must send the video sticker as a document STICKER_VIDEO_NOWEBM,400,The specified video sticker is not in webm format STORAGE_CHECK_FAILED,500,Server storage check failed STORE_INVALID_SCALAR_TYPE,500, SWITCH_PM_TEXT_EMPTY,400,The switch_pm.text field was empty TAKEOUT_INIT_DELAY_X,420,A wait of {seconds} seconds is required before being able to initiate the takeout TAKEOUT_INVALID,400,The takeout session has been invalidated by another data export session TAKEOUT_REQUIRED,400 403,You must initialize a takeout request first TEMP_AUTH_KEY_ALREADY_BOUND,400,The passed temporary key is already bound to another **perm_auth_key_id** TEMP_AUTH_KEY_EMPTY,400,No temporary auth key provided THEME_FILE_INVALID,400,Invalid theme file provided THEME_FORMAT_INVALID,400,Invalid theme format provided THEME_INVALID,400,Theme invalid THEME_MIME_INVALID,400,"You cannot create this theme, the mime-type is invalid" THEME_TITLE_INVALID,400,The specified theme title is invalid TIMEOUT,500,A timeout occurred while fetching data from the worker TITLE_INVALID,400,The specified stickerpack title is invalid TMP_PASSWORD_DISABLED,400,The temporary password is disabled TMP_PASSWORD_INVALID,400,Password auth needs to be regenerated TOKEN_INVALID,400,The provided token is invalid TOPIC_DELETED,400,The topic was deleted TO_LANG_INVALID,400,The specified destination language is invalid TTL_DAYS_INVALID,400,The provided TTL is invalid TTL_MEDIA_INVALID,400,The provided media cannot be used with a TTL TTL_PERIOD_INVALID,400,The provided TTL Period is invalid TYPES_EMPTY,400,The types field is empty TYPE_CONSTRUCTOR_INVALID,400,The type constructor is invalid Timedout,-503,Timeout while fetching data Timeout,-503,Timeout while fetching data UNKNOWN_ERROR,400, UNKNOWN_METHOD,500,The method you tried to call cannot be called on non-CDN DCs UNTIL_DATE_INVALID,400,That date cannot be specified in this request (try using None) UPDATE_APP_TO_LOGIN,406, URL_INVALID,400,The URL used was invalid (e.g. when answering a callback with a URL that's not t.me/yourbot or your game's URL) USAGE_LIMIT_INVALID,400,The specified usage limit is invalid USERNAME_INVALID,400,"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,400,The username is not different from the current username USERNAME_NOT_OCCUPIED,400,The username is not in use by anyone else yet USERNAME_OCCUPIED,400,The username is already taken USERNAME_PURCHASE_AVAILABLE,400, USERPIC_PRIVACY_REQUIRED,406,You need to disable privacy settings for your profile picture in order to make your geolocation public USERPIC_UPLOAD_REQUIRED,400 406,You must have a profile picture before using this method USERS_TOO_FEW,400,"Not enough users (to create a chat, for example)" USERS_TOO_MUCH,400,"The maximum number of users has been exceeded (to create a chat, for example)" USER_ADMIN_INVALID,400,Either you're not an admin or you tried to ban an admin that you didn't promote USER_ALREADY_INVITED,400,You have already invited this user USER_ALREADY_PARTICIPANT,400,The authenticated user is already a participant of the chat USER_BANNED_IN_CHANNEL,400,You're banned from sending messages in supergroups/channels USER_BLOCKED,400,User blocked USER_BOT,400,Bots can only be admins in channels. USER_BOT_INVALID,400 403,This method can only be called by a bot USER_BOT_REQUIRED,400,This method can only be called by a bot USER_CHANNELS_TOO_MUCH,400 403,One of the users you tried to add is already in too many channels/supergroups USER_CREATOR,400,"You can't leave this channel, because you're its creator" USER_DEACTIVATED,401,The user has been deleted/deactivated USER_DEACTIVATED_BAN,401,The user has been deleted/deactivated USER_DELETED,403,You can't send this secret message because the other participant deleted their account USER_ID_INVALID,400,"Invalid object ID for a user. Make sure to pass the right types, for instance making sure that the request is designed for users or otherwise look for a different one more suited" USER_INVALID,400 403,The given user was invalid USER_IS_BLOCKED,400 403,User is blocked USER_IS_BOT,400,Bots can't send messages to other bots USER_KICKED,400,This user was kicked from this supergroup/channel USER_MIGRATE_X,303,The user whose identity is being used to execute queries is associated with DC {new_dc} USER_NOT_MUTUAL_CONTACT,400 403,The provided user is not a mutual contact USER_NOT_PARTICIPANT,400,The target user is not a member of the specified megagroup or channel USER_PRIVACY_RESTRICTED,403,The user's privacy settings do not allow you to do this USER_RESTRICTED,403 406,"You're spamreported, you can't create channels or chats." USER_VOLUME_INVALID,400,The specified user volume is invalid VIDEO_CONTENT_TYPE_INVALID,400,The video content type is not supported with the given parameters (i.e. supports_streaming) VIDEO_FILE_INVALID,400,The given video cannot be used VIDEO_TITLE_EMPTY,400,The specified video title is empty VOICE_MESSAGES_FORBIDDEN,400,This user's privacy settings forbid you from sending voice messages WALLPAPER_FILE_INVALID,400,The given file cannot be used as a wallpaper WALLPAPER_INVALID,400,The input wallpaper was not valid WALLPAPER_MIME_INVALID,400,The specified wallpaper MIME type is invalid WC_CONVERT_URL_INVALID,400,WC convert URL invalid WEBDOCUMENT_INVALID,400,Invalid webdocument URL provided WEBDOCUMENT_MIME_INVALID,400,Invalid webdocument mime type provided WEBDOCUMENT_SIZE_TOO_BIG,400,Webdocument is too big! WEBDOCUMENT_URL_INVALID,400,The given URL cannot be used WEBPAGE_CURL_FAILED,400,Failure while fetching the webpage with cURL WEBPAGE_MEDIA_EMPTY,400,Webpage media empty WEBPUSH_AUTH_INVALID,400,The specified web push authentication secret is invalid WEBPUSH_KEY_INVALID,400,The specified web push elliptic curve Diffie-Hellman public key is invalid WEBPUSH_TOKEN_INVALID,400,The specified web push token is invalid WORKER_BUSY_TOO_LONG_RETRY,500,Telegram workers are too busy to respond immediately YOU_BLOCKED_USER,400,You blocked this user Telethon-1.39.0/telethon_generator/data/friendly.csv000066400000000000000000000035321475566265000225430ustar00rootroot00000000000000ns,friendly,raw account.AccountMethods,takeout,invokeWithTakeout account.initTakeoutSession account.finishTakeoutSession auth.AuthMethods,sign_in,auth.signIn auth.importBotAuthorization auth.AuthMethods,send_code_request,auth.sendCode auth.resendCode auth.AuthMethods,log_out,auth.logOut auth.AuthMethods,edit_2fa,account.updatePasswordSettings bots.BotMethods,inline_query,messages.getInlineBotResults chats.ChatMethods,action,messages.setTyping chats.ChatMethods,edit_admin,channels.editAdmin messages.editChatAdmin chats.ChatMethods,edit_permissions,channels.editBanned messages.editChatDefaultBannedRights chats.ChatMethods,iter_participants,channels.getParticipants chats.ChatMethods,iter_admin_log,channels.getAdminLog dialogs.DialogMethods,iter_dialogs,messages.getDialogs dialogs.DialogMethods,iter_drafts,messages.getAllDrafts dialogs.DialogMethods,edit_folder,folders.deleteFolder folders.editPeerFolders downloads.DownloadMethods,download_media,upload.getFile messages.MessageMethods,iter_messages,messages.searchGlobal messages.search messages.getHistory channels.getMessages messages.getMessages messages.MessageMethods,send_message,messages.sendMessage messages.MessageMethods,forward_messages,messages.forwardMessages messages.MessageMethods,edit_message,messages.editInlineBotMessage messages.editMessage messages.MessageMethods,delete_messages,channels.deleteMessages messages.deleteMessages messages.MessageMethods,send_read_acknowledge,messages.readMentions channels.readHistory messages.readHistory updates.UpdateMethods,catch_up,updates.getDifference updates.getChannelDifference uploads.UploadMethods,send_file,messages.sendMedia messages.sendMultiMedia messages.uploadMedia uploads.UploadMethods,upload_file,upload.saveFilePart upload.saveBigFilePart users.UserMethods,get_entity,users.getUsers messages.getChats channels.getChannels contacts.resolveUsername Telethon-1.39.0/telethon_generator/data/html/000077500000000000000000000000001475566265000211535ustar00rootroot00000000000000Telethon-1.39.0/telethon_generator/data/html/404.html000066400000000000000000000021251475566265000223500ustar00rootroot00000000000000 Oopsie! | Telethon

You seem a bit lost…

You seem to be lost! Don't worry, that's just Telegram's API being itself. Shall we go back to the Main Page?

Telethon-1.39.0/telethon_generator/data/html/core.html000066400000000000000000000170621475566265000227770ustar00rootroot00000000000000 Telethon API

Telethon API

This documentation was generated straight from the scheme.tl provided by Telegram. However, there is no official documentation per se on what the methods, constructors and types mean. Nevertheless, this page aims to provide easy access to all the available methods, their definition and parameters.

light / dark theme.

Please note that when you see this:

---functions---
users.getUsers#0d91a548 id:Vector<InputUser> = Vector<User>

This is not Python code. It's the "TL definition". It's an easy-to-read line that gives a quick overview on the parameters and its result. You don't need to worry about this. See Understanding the Type Language for more details on it.

Index

Methods

Currently there are {method_count} methods available for the layer {layer}. See the complete method list.

Methods, also known as requests, are used to interact with the Telegram API itself and are invoked through client(Request(...)). Only these can be used like that! You cannot invoke types or constructors, only requests. After this, Telegram will return a result, which may be, for instance, a bunch of messages, some dialogs, users, etc.

Types

Currently there are {type_count} types. See the complete list of types.

The Telegram types are the abstract results that you receive after invoking a request. They are "abstract" because they can have multiple constructors. For instance, the abstract type User can be either UserEmpty or User. You should, most of the time, make sure you received the desired type by using the isinstance(result, Constructor) Python function. When a request needs a Telegram type as argument, you should create an instance of it by using one of its, possibly multiple, constructors.

Constructors

Currently there are {constructor_count} constructors. See the list of all constructors.

Constructors are the way you can create instances of the abstract types described above, and also the instances which are actually returned from the functions although they all share a common abstract type.

Core types

Core types are types from which the rest of Telegram types build upon:

  • int: The value should be an integer type, like 42. It should have 32 bits or less. You can check the bit length by calling a.bit_length(), where a is an integer variable.
  • long: Different name for an integer type. The numbers given should have 64 bits or less.
  • int128: Another integer type, should have 128 bits or less.
  • int256: The largest integer type, allowing 256 bits or less.
  • double: The value should be a floating point value, such as 123.456.
  • Vector<T>: If a type T is wrapped around Vector<T>, then it means that the argument should be a list of it. For instance, a valid value for Vector<int> would be [1, 2, 3].
  • string: A valid UTF-8 string should be supplied. This is right how Python strings work, no further encoding is required.
  • Bool: Either True or False.
  • flag: These arguments aren't actually sent but rather encoded as flags. Any truthy value (True, 7) will enable this flag, although it's recommended to use True or None to symbolize that it's not present.
  • bytes: A sequence of bytes, like b'hello', should be supplied.
  • date: Although this type is internally used as an int, you can pass a datetime or date object instead to work with date parameters.
    Note that the library uses the date in UTC+0, since timezone conversion is not responsibility of the library. Furthermore, this eases converting into any other timezone without the need for a middle step.

Full example

All methods shown here have dummy examples on how to write them, so you don't get confused with their TL definition. However, this may not always run. They are just there to show the right syntax.

You should check out how to access the full API in ReadTheDocs.

Telethon-1.39.0/telethon_generator/data/html/css/000077500000000000000000000000001475566265000217435ustar00rootroot00000000000000Telethon-1.39.0/telethon_generator/data/html/css/docs.dark.css000066400000000000000000000053741475566265000243360ustar00rootroot00000000000000body { font-family: 'Nunito', sans-serif; color: #bbb; background-color:#000; font-size: 16px; } a { color: #42aaed; text-decoration: none; } pre { font-family: 'Source Code Pro', monospace; padding: 8px; color: #567; background: #080a0c; border-radius: 0; overflow-x: auto; } a:hover { color: #64bbdd; text-decoration: underline; } table { width: 100%; max-width: 100%; } table td { border-top: 1px solid #111; padding: 8px; } .horizontal { margin-bottom: 16px; list-style: none; background: #080a0c; border-radius: 4px; padding: 8px 16px; } .horizontal li { display: inline-block; margin: 0 8px 0 0; } .horizontal img { display: inline-block; margin: 0 8px -2px 0; } h1, summary.title { font-size: 24px; } h3 { font-size: 20px; } #main_div { padding: 20px 0; max-width: 800px; margin: 0 auto; } pre::-webkit-scrollbar { visibility: visible; display: block; height: 12px; } pre::-webkit-scrollbar-track:horizontal { background: #222; border-radius: 0; height: 12px; } pre::-webkit-scrollbar-thumb:horizontal { background: #444; border-radius: 0; height: 12px; } :target { border: 2px solid #149; background: #246; padding: 4px; } /* 'sh' stands for Syntax Highlight */ span.sh1 { color: #f93; } span.tooltip { border-bottom: 1px dashed #ddd; } #searchBox { width: 100%; border: none; height: 20px; padding: 8px; font-size: 16px; border-radius: 2px; border: 2px solid #222; background: #000; color: #eee; } #searchBox:placeholder-shown { color: #bbb; font-style: italic; } button { border-radius: 2px; font-size: 16px; padding: 8px; color: #bbb; background-color: #111; border: 2px solid #146; transition-duration: 300ms; } button:hover { background-color: #146; color: #fff; } /* https://www.w3schools.com/css/css_navbar.asp */ ul.together { list-style-type: none; margin: 0; padding: 0; overflow: hidden; } ul.together li { float: left; } ul.together li a { display: block; border-radius: 8px; background: #111; padding: 4px 8px; margin: 8px; } /* https://stackoverflow.com/a/30810322 */ .invisible { left: 0; top: -99px; padding: 0; width: 2em; height: 2em; border: none; outline: none; position: fixed; box-shadow: none; color: transparent; background: transparent; } @media (max-width: 640px) { h1, summary.title { font-size: 18px; } h3 { font-size: 16px; } #dev_page_content_wrap { padding-top: 12px; } #dev_page_title { margin-top: 10px; margin-bottom: 20px; } } Telethon-1.39.0/telethon_generator/data/html/css/docs.h4x0r.css000066400000000000000000000076661475566265000243700ustar00rootroot00000000000000/* Begin of https://cdn.jsdelivr.net/npm/hack-font@3/build/web/hack.css * * Hack typeface https://github.com/source-foundry/Hack * License: https://github.com/source-foundry/Hack/blob/master/LICENSE.md */ @font-face { font-family: 'Hack'; src: url('fonts/hack-regular.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-regular.woff?sha=3114f1256') format('woff'); font-weight: 400; font-style: normal; } @font-face { font-family: 'Hack'; src: url('fonts/hack-bold.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-bold.woff?sha=3114f1256') format('woff'); font-weight: 700; font-style: normal; } @font-face { font-family: 'Hack'; src: url('fonts/hack-italic.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-italic.woff?sha=3114f1256') format('woff'); font-weight: 400; font-style: italic; } @font-face { font-family: 'Hack'; src: url('fonts/hack-bolditalic.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-bolditalic.woff?sha=3114f1256') format('woff'); font-weight: 700; font-style: italic; } /* End of https://cdn.jsdelivr.net/npm/hack-font@3/build/web/hack.css */ body { font-family: 'Hack', monospace; color: #0a0; background-color: #000; font-size: 16px; } ::-moz-selection { color: #000; background: #0a0; } ::selection { color: #000; background: #0a0; } a { color: #0a0; } pre { padding: 8px; color: #0c0; background: #010; border-radius: 0; overflow-x: auto; } a:hover { color: #0f0; text-decoration: underline; } table { width: 100%; max-width: 100%; } table td { border-top: 1px solid #111; padding: 8px; } .horizontal { margin-bottom: 16px; list-style: none; background: #010; border-radius: 4px; padding: 8px 16px; } .horizontal li { display: inline-block; margin: 0 8px 0 0; } .horizontal img { opacity: 0; display: inline-block; margin: 0 8px -2px 0; } h1, summary.title { font-size: 24px; } h3 { font-size: 20px; } #main_div { padding: 20px 0; max-width: 800px; margin: 0 auto; } pre::-webkit-scrollbar { visibility: visible; display: block; height: 12px; } pre::-webkit-scrollbar-track:horizontal { background: #222; border-radius: 0; height: 12px; } pre::-webkit-scrollbar-thumb:horizontal { background: #444; border-radius: 0; height: 12px; } :target { border: 2px solid #0f0; background: #010; padding: 4px; } /* 'sh' stands for Syntax Highlight */ span.sh1 { color: #0f0; } span.tooltip { border-bottom: 1px dashed #ddd; } #searchBox { width: 100%; border: none; height: 20px; padding: 8px; font-size: 16px; border-radius: 2px; border: 2px solid #222; background: #000; color: #0e0; font-family: 'Hack', monospace; } #searchBox:placeholder-shown { color: #0b0; font-style: italic; } button { font-size: 16px; padding: 8px; color: #0f0; background-color: #071007; border: 2px solid #131; transition-duration: 300ms; font-family: 'Hack', monospace; } button:hover { background-color: #131; } /* https://www.w3schools.com/css/css_navbar.asp */ ul.together { list-style-type: none; margin: 0; padding: 0; overflow: hidden; } ul.together li { float: left; } ul.together li a { display: block; border-radius: 8px; background: #121; padding: 4px 8px; margin: 8px; } /* https://stackoverflow.com/a/30810322 */ .invisible { left: 0; top: -99px; padding: 0; width: 2em; height: 2em; border: none; outline: none; position: fixed; box-shadow: none; color: transparent; background: transparent; } @media (max-width: 640px) { h1, summary.title { font-size: 18px; } h3 { font-size: 16px; } #dev_page_content_wrap { padding-top: 12px; } #dev_page_title { margin-top: 10px; margin-bottom: 20px; } } Telethon-1.39.0/telethon_generator/data/html/css/docs.light.css000066400000000000000000000053311475566265000245150ustar00rootroot00000000000000body { font-family: 'Nunito', sans-serif; color: #333; background-color:#eee; font-size: 16px; } a { color: #329add; text-decoration: none; } pre { font-family: 'Source Code Pro', monospace; padding: 8px; color: #567; background: #e0e4e8; border-radius: 0; overflow-x: auto; } a:hover { color: #64bbdd; text-decoration: underline; } table { width: 100%; max-width: 100%; } table td { border-top: 1px solid #ddd; padding: 8px; } .horizontal { margin-bottom: 16px; list-style: none; background: #e0e4e8; border-radius: 4px; padding: 8px 16px; } .horizontal li { display: inline-block; margin: 0 8px 0 0; } .horizontal img { display: inline-block; margin: 0 8px -2px 0; } h1, summary.title { font-size: 24px; } h3 { font-size: 20px; } #main_div { padding: 20px 0; max-width: 800px; margin: 0 auto; } pre::-webkit-scrollbar { visibility: visible; display: block; height: 12px; } pre::-webkit-scrollbar-track:horizontal { background: #def; border-radius: 0; height: 12px; } pre::-webkit-scrollbar-thumb:horizontal { background: #bdd; border-radius: 0; height: 12px; } :target { border: 2px solid #f8f800; background: #f8f8f8; padding: 4px; } /* 'sh' stands for Syntax Highlight */ span.sh1 { color: #f70; } span.tooltip { border-bottom: 1px dashed #444; } #searchBox { width: 100%; border: none; height: 20px; padding: 8px; font-size: 16px; border-radius: 2px; border: 2px solid #ddd; } #searchBox:placeholder-shown { font-style: italic; } button { border-radius: 2px; font-size: 16px; padding: 8px; color: #000; background-color: #f7f7f7; border: 2px solid #329add; transition-duration: 300ms; } button:hover { background-color: #329add; color: #f7f7f7; } /* https://www.w3schools.com/css/css_navbar.asp */ ul.together { list-style-type: none; margin: 0; padding: 0; overflow: hidden; } ul.together li { float: left; } ul.together li a { display: block; border-radius: 8px; background: #e0e4e8; padding: 4px 8px; margin: 8px; } /* https://stackoverflow.com/a/30810322 */ .invisible { left: 0; top: -99px; padding: 0; width: 2em; height: 2em; border: none; outline: none; position: fixed; box-shadow: none; color: transparent; background: transparent; } @media (max-width: 640px) { h1, summary.title { font-size: 18px; } h3 { font-size: 16px; } #dev_page_content_wrap { padding-top: 12px; } #dev_page_title { margin-top: 10px; margin-bottom: 20px; } } Telethon-1.39.0/telethon_generator/data/html/img/000077500000000000000000000000001475566265000217275ustar00rootroot00000000000000Telethon-1.39.0/telethon_generator/data/html/img/arrow.svg000066400000000000000000000020441475566265000236020ustar00rootroot00000000000000 image/svg+xml Telethon-1.39.0/telethon_generator/data/html/js/000077500000000000000000000000001475566265000215675ustar00rootroot00000000000000Telethon-1.39.0/telethon_generator/data/html/js/search.js000066400000000000000000000165011475566265000233750ustar00rootroot00000000000000root = document.getElementById("main_div"); root.innerHTML = `
Methods (0)
Types (0)
Constructors (0)
` + root.innerHTML + "
"; // HTML modified, now load documents contentDiv = document.getElementById("contentDiv"); searchDiv = document.getElementById("searchDiv"); searchBox = document.getElementById("searchBox"); // Search lists methodsDetails = document.getElementById("methods"); methodsList = document.getElementById("methodsList"); methodsCount = document.getElementById("methodsCount"); typesDetails = document.getElementById("types"); typesList = document.getElementById("typesList"); typesCount = document.getElementById("typesCount"); constructorsDetails = document.getElementById("constructors"); constructorsList = document.getElementById("constructorsList"); constructorsCount = document.getElementById("constructorsCount"); // Exact match exactMatch = document.getElementById("exactMatch"); exactList = document.getElementById("exactList"); try { requests = [{request_names}]; types = [{type_names}]; constructors = [{constructor_names}]; requestsu = [{request_urls}]; typesu = [{type_urls}]; constructorsu = [{constructor_urls}]; } catch (e) { requests = []; types = []; constructors = []; requestsu = []; typesu = []; constructorsu = []; } if (typeof prependPath !== 'undefined') { for (var i = 0; i != requestsu.length; ++i) { requestsu[i] = prependPath + requestsu[i]; } for (var i = 0; i != typesu.length; ++i) { typesu[i] = prependPath + typesu[i]; } for (var i = 0; i != constructorsu.length; ++i) { constructorsu[i] = prependPath + constructorsu[i]; } } // Assumes haystack has no whitespace and both are lowercase. // // Returns the penalty for finding the needle in the haystack // or -1 if the needle wasn't found at all. function find(haystack, needle) { if (haystack.indexOf(needle) != -1) { return 0; } var hi = 0; var ni = 0; var penalty = 0; var started = false; while (true) { while (needle[ni] < 'a' || needle[ni] > 'z') { ++ni; if (ni == needle.length) { return penalty; } } while (haystack[hi] != needle[ni]) { ++hi; if (started) { ++penalty; } if (hi == haystack.length) { return -1; } } ++hi; ++ni; started = true; if (ni == needle.length) { return penalty; } if (hi == haystack.length) { return -1; } } } // Given two input arrays "original" and "original urls" and a query, // return a pair of arrays with matching "query" elements from "original". // // TODO Perhaps return an array of pairs instead a pair of arrays (for cache). function getSearchArray(original, originalu, query) { var destination = []; var destinationu = []; for (var i = 0; i < original.length; ++i) { var penalty = find(original[i].toLowerCase(), query); if (penalty > -1 && penalty < original[i].length / 3) { destination.push(original[i]); destinationu.push(originalu[i]); } } return [destination, destinationu]; } // Modify "countSpan" and "resultList" accordingly based on the elements // given as [[elements], [element urls]] (both with the same length) function buildList(countSpan, resultList, foundElements) { var result = ""; for (var i = 0; i < foundElements[0].length; ++i) { result += '
  • '; result += ''; result += foundElements[0][i]; result += '
  • '; } if (countSpan) { countSpan.innerHTML = "" + foundElements[0].length; } resultList.innerHTML = result; } function updateSearch(event) { var query = searchBox.value.toLowerCase(); if (!query) { contentDiv.style.display = ""; searchDiv.style.display = "none"; return; } contentDiv.style.display = "none"; searchDiv.style.display = ""; var foundRequests = getSearchArray(requests, requestsu, query); var foundTypes = getSearchArray(types, typesu, query); var foundConstructors = getSearchArray(constructors, constructorsu, query); var original = requests.concat(constructors); var originalu = requestsu.concat(constructorsu); var destination = []; var destinationu = []; for (var i = 0; i < original.length; ++i) { if (original[i].toLowerCase().replace("request", "") == query) { destination.push(original[i]); destinationu.push(originalu[i]); } } if (event && event.keyCode == 13) { if (destination.length != 0) { window.location = destinationu[0]; } else if (methodsDetails.open && foundRequests[1].length) { window.location = foundRequests[1][0]; } else if (typesDetails.open && foundTypes[1].length) { window.location = foundTypes[1][0]; } else if (constructorsDetails.open && foundConstructors[1].length) { window.location = foundConstructors[1][0]; } return; } buildList(methodsCount, methodsList, foundRequests); buildList(typesCount, typesList, foundTypes); buildList(constructorsCount, constructorsList, foundConstructors); // Now look for exact matches if (destination.length == 0) { exactMatch.style.display = "none"; } else { exactMatch.style.display = ""; buildList(null, exactList, [destination, destinationu]); return destinationu[0]; } } function getQuery(name) { var query = window.location.search.substring(1); var vars = query.split("&"); for (var i = 0; i != vars.length; ++i) { var pair = vars[i].split("="); if (pair[0] == name) return decodeURI(pair[1]); } } document.onkeydown = function (e) { if (e.key == '/' || e.key == 's' || e.key == 'S') { if (document.activeElement != searchBox) { searchBox.focus(); return false; } } else if (e.key == '?') { alert('Pressing any of: /sS\nWill focus the search bar\n\n' + 'Pressing: enter\nWill navigate to the first match') } } var query = getQuery('q'); if (query) { searchBox.value = query; } var exactUrl = updateSearch(); var redirect = getQuery('redirect'); if (exactUrl && redirect != 'no') { window.location = exactUrl; } Telethon-1.39.0/telethon_generator/data/methods.csv000066400000000000000000000600511475566265000223710ustar00rootroot00000000000000method,usability,errors account.acceptAuthorization,user, account.cancelPasswordEmail,user, account.changePhone,user,PHONE_NUMBER_INVALID account.checkUsername,user,USERNAME_INVALID account.confirmPasswordEmail,user, account.confirmPhone,user,CODE_HASH_INVALID PHONE_CODE_EMPTY account.createTheme,user,THEME_MIME_INVALID account.declinePasswordReset,user,RESET_REQUEST_MISSING account.deleteAccount,user,2FA_CONFIRM_WAIT_X account.deleteSecureValue,user, account.finishTakeoutSession,user, account.getAccountTTL,user, account.getAllSecureValues,user, account.getAuthorizationForm,user,PUBLIC_KEY_REQUIRED account.getAuthorizations,user, account.getAutoDownloadSettings,user, account.getContactSignUpNotification,user, account.getContentSettings,user, account.getMultiWallPapers,user, account.getNotifyExceptions,user, account.getNotifySettings,user,PEER_ID_INVALID account.getPassword,user, account.getPasswordSettings,user,PASSWORD_HASH_INVALID account.getPrivacy,user,PRIVACY_KEY_INVALID account.getSecureValue,user, account.getTheme,user, account.getThemes,user, account.getTmpPassword,user,PASSWORD_HASH_INVALID TMP_PASSWORD_DISABLED account.getWallPaper,user,WALLPAPER_INVALID account.getWallPapers,user, account.getWebAuthorizations,user, account.initTakeoutSession,user, account.installTheme,user, account.installWallPaper,user,WALLPAPER_INVALID account.registerDevice,user,TOKEN_INVALID account.reportPeer,user,PEER_ID_INVALID account.resendPasswordEmail,user, account.resetAuthorization,user,HASH_INVALID account.resetNotifySettings,user, account.resetWallPapers,user, account.resetWebAuthorization,user, account.resetWebAuthorizations,user, account.saveAutoDownloadSettings,user, account.saveSecureValue,user,PASSWORD_REQUIRED account.saveTheme,user, account.saveWallPaper,user,WALLPAPER_INVALID account.sendChangePhoneCode,user,FRESH_CHANGE_PHONE_FORBIDDEN PHONE_NUMBER_INVALID account.sendConfirmPhoneCode,user,HASH_INVALID account.sendVerifyEmailCode,user,EMAIL_INVALID account.sendVerifyPhoneCode,user, account.setAccountTTL,user,TTL_DAYS_INVALID account.setContactSignUpNotification,user, account.setContentSettings,user,SENSITIVE_CHANGE_FORBIDDEN account.setGlobalPrivacySettings,user,AUTOARCHIVE_NOT_AVAILABLE account.setPrivacy,user,PRIVACY_KEY_INVALID PRIVACY_TOO_LONG account.unregisterDevice,user,TOKEN_INVALID account.updateDeviceLocked,user, account.updateNotifySettings,user,PEER_ID_INVALID account.updatePasswordSettings,user,EMAIL_UNCONFIRMED_X NEW_SALT_INVALID NEW_SETTINGS_INVALID PASSWORD_HASH_INVALID account.updateProfile,user,ABOUT_TOO_LONG FIRSTNAME_INVALID account.updateStatus,user,SESSION_PASSWORD_NEEDED account.updateTheme,user,THEME_INVALID account.updateUsername,user,USERNAME_INVALID USERNAME_NOT_MODIFIED USERNAME_OCCUPIED account.uploadTheme,user, account.uploadWallPaper,user,WALLPAPER_FILE_INVALID WALLPAPER_MIME_INVALID account.verifyEmail,user,EMAIL_INVALID account.verifyPhone,user, auth.acceptLoginToken,user, auth.bindTempAuthKey,both,ENCRYPTED_MESSAGE_INVALID INPUT_REQUEST_TOO_LONG TEMP_AUTH_KEY_EMPTY TIMEOUT auth.cancelCode,user,PHONE_NUMBER_INVALID auth.checkPassword,user,PASSWORD_HASH_INVALID auth.checkRecoveryPassword,user,PASSWORD_RECOVERY_EXPIRED auth.dropTempAuthKeys,both, auth.exportAuthorization,both,DC_ID_INVALID auth.exportLoginToken,user, auth.importAuthorization,both,AUTH_BYTES_INVALID USER_ID_INVALID auth.importBotAuthorization,both,ACCESS_TOKEN_EXPIRED ACCESS_TOKEN_INVALID API_ID_INVALID auth.importLoginToken,user,AUTH_TOKEN_ALREADY_ACCEPTED AUTH_TOKEN_EXPIRED AUTH_TOKEN_INVALID auth.logOut,both, auth.recoverPassword,user,CODE_EMPTY NEW_SETTINGS_INVALID auth.requestPasswordRecovery,user,PASSWORD_EMPTY PASSWORD_RECOVERY_NA auth.resendCode,user,PHONE_NUMBER_INVALID SEND_CODE_UNAVAILABLE auth.resetAuthorizations,user,TIMEOUT auth.sendCode,user,API_ID_INVALID API_ID_PUBLISHED_FLOOD AUTH_RESTART INPUT_REQUEST_TOO_LONG PHONE_NUMBER_APP_SIGNUP_FORBIDDEN PHONE_NUMBER_BANNED PHONE_NUMBER_FLOOD PHONE_NUMBER_INVALID PHONE_PASSWORD_FLOOD PHONE_PASSWORD_PROTECTED auth.signIn,user,PHONE_CODE_EMPTY PHONE_CODE_EXPIRED PHONE_CODE_INVALID PHONE_NUMBER_INVALID PHONE_NUMBER_UNOCCUPIED SESSION_PASSWORD_NEEDED auth.signUp,user,FIRSTNAME_INVALID MEMBER_OCCUPY_PRIMARY_LOC_FAILED PHONE_CODE_EMPTY PHONE_CODE_EXPIRED PHONE_CODE_INVALID PHONE_NUMBER_FLOOD PHONE_NUMBER_INVALID PHONE_NUMBER_OCCUPIED REG_ID_GENERATE_FAILED bots.answerWebhookJSONQuery,bot,QUERY_ID_INVALID USER_BOT_INVALID bots.sendCustomRequest,bot,USER_BOT_INVALID bots.setBotCommands,bot,BOT_COMMAND_DESCRIPTION_INVALID BOT_COMMAND_INVALID LANG_CODE_INVALID channels.checkUsername,user,CHANNEL_INVALID CHAT_ID_INVALID USERNAME_INVALID channels.convertToGigagroup,user,PARTICIPANTS_TOO_FEW channels.createChannel,user,CHAT_TITLE_EMPTY USER_RESTRICTED channels.deleteChannel,user,CHANNEL_INVALID CHANNEL_PRIVATE CHANNEL_TOO_LARGE channels.deleteHistory,user,CHANNEL_TOO_BIG channels.deleteMessages,both,CHANNEL_INVALID CHANNEL_PRIVATE MESSAGE_DELETE_FORBIDDEN channels.deleteUserHistory,user,CHANNEL_INVALID CHAT_ADMIN_REQUIRED channels.editAdmin,both,ADMINS_TOO_MUCH ADMIN_RANK_EMOJI_NOT_ALLOWED ADMIN_RANK_INVALID BOT_CHANNELS_NA CHANNEL_INVALID CHAT_ADMIN_INVITE_REQUIRED CHAT_ADMIN_REQUIRED FRESH_CHANGE_ADMINS_FORBIDDEN RIGHT_FORBIDDEN USER_CREATOR USER_ID_INVALID USER_NOT_MUTUAL_CONTACT USER_PRIVACY_RESTRICTED channels.editBanned,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED USER_ADMIN_INVALID USER_ID_INVALID channels.editCreator,user,PASSWORD_MISSING PASSWORD_TOO_FRESH_X SESSION_TOO_FRESH_X SRP_ID_INVALID channels.editLocation,user, channels.editPhoto,both,CHANNEL_INVALID CHAT_ADMIN_REQUIRED FILE_REFERENCE_INVALID PHOTO_INVALID channels.editTitle,both,CHANNEL_INVALID CHAT_ADMIN_REQUIRED CHAT_NOT_MODIFIED channels.exportMessageLink,user,CHANNEL_INVALID channels.getAdminLog,user,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED channels.getAdminedPublicChannels,user, channels.getChannels,both,CHANNEL_INVALID CHANNEL_PRIVATE NEED_CHAT_INVALID channels.getFullChannel,both,CHANNEL_INVALID CHANNEL_PRIVATE CHANNEL_PUBLIC_GROUP_NA TIMEOUT channels.getGroupsForDiscussion,user, channels.getInactiveChannels,user, channels.getLeftChannels,user, channels.getMessages,both,CHANNEL_INVALID CHANNEL_PRIVATE MESSAGE_IDS_EMPTY channels.getParticipant,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED USER_ID_INVALID USER_NOT_PARTICIPANT channels.getParticipants,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED INPUT_CONSTRUCTOR_INVALID TIMEOUT channels.inviteToChannel,user,BOTS_TOO_MUCH BOT_GROUPS_BLOCKED CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_INVALID CHAT_WRITE_FORBIDDEN INPUT_USER_DEACTIVATED USERS_TOO_MUCH USER_BANNED_IN_CHANNEL USER_BLOCKED USER_BOT USER_CHANNELS_TOO_MUCH USER_ID_INVALID USER_KICKED USER_NOT_MUTUAL_CONTACT USER_PRIVACY_RESTRICTED channels.joinChannel,user,CHANNELS_TOO_MUCH CHANNEL_INVALID CHANNEL_PRIVATE INVITE_REQUEST_SENT channels.leaveChannel,both,CHANNEL_INVALID CHANNEL_PRIVATE CHANNEL_PUBLIC_GROUP_NA USER_CREATOR USER_NOT_PARTICIPANT channels.readHistory,user,CHANNEL_INVALID CHANNEL_PRIVATE channels.readMessageContents,user,CHANNEL_INVALID CHANNEL_PRIVATE channels.reportSpam,user,CHANNEL_INVALID INPUT_USER_DEACTIVATED channels.setDiscussionGroup,user,BROADCAST_ID_INVALID LINK_NOT_MODIFIED MEGAGROUP_ID_INVALID MEGAGROUP_PREHISTORY_HIDDEN channels.setStickers,both,CHANNEL_INVALID PARTICIPANTS_TOO_FEW STICKERSET_OWNER_ANONYMOUS channels.toggleForum,user,CHAT_DISCUSSION_UNALLOWED channels.togglePreHistoryHidden,user,CHAT_LINK_EXISTS channels.toggleSignatures,user,CHANNEL_INVALID channels.toggleSlowMode,user,SECONDS_INVALID channels.updateUsername,user,CHANNELS_ADMIN_PUBLIC_TOO_MUCH CHANNEL_INVALID CHAT_ADMIN_REQUIRED USERNAME_INVALID USERNAME_OCCUPIED USERNAME_PURCHASE_AVAILABLE channels.viewSponsoredMessage,user,UNKNOWN_ERROR contacts.acceptContact,user, contacts.addContact,user,CONTACT_NAME_EMPTY contacts.block,user,CONTACT_ID_INVALID contacts.deleteByPhones,user, contacts.deleteContacts,user,NEED_MEMBER_INVALID TIMEOUT contacts.getBlocked,user, contacts.getContactIDs,user, contacts.getContacts,user, contacts.getLocated,user,USERPIC_UPLOAD_REQUIRED contacts.getSaved,user,TAKEOUT_REQUIRED contacts.getStatuses,user, contacts.getTopPeers,user,TYPES_EMPTY contacts.importContacts,user, contacts.resetSaved,user, contacts.resetTopPeerRating,user,PEER_ID_INVALID contacts.resolveUsername,both,AUTH_KEY_PERM_EMPTY SESSION_PASSWORD_NEEDED USERNAME_INVALID USERNAME_NOT_OCCUPIED contacts.search,user,QUERY_TOO_SHORT SEARCH_QUERY_EMPTY TIMEOUT contacts.toggleTopPeers,user, contacts.unblock,user,CONTACT_ID_INVALID folders.deleteFolder,user,FOLDER_ID_EMPTY folders.editPeerFolders,user,FOLDER_ID_INVALID getFutureSalts,both, help.acceptTermsOfService,user, help.editUserInfo,user,ENTITY_BOUNDS_INVALID USER_INVALID help.getAppChangelog,user, help.getAppConfig,user, help.getAppUpdate,user, help.getCdnConfig,both,AUTH_KEY_PERM_EMPTY TIMEOUT help.getConfig,both,AUTH_KEY_DUPLICATED TIMEOUT help.getDeepLinkInfo,user, help.getInviteText,user, help.getNearestDc,user, help.getPassportConfig,user, help.getProxyData,user, help.getRecentMeUrls,user, help.getSupport,user, help.getSupportName,user,USER_INVALID help.getTermsOfServiceUpdate,user, help.getUserInfo,user,USER_INVALID help.saveAppLog,user, help.setBotUpdatesStatus,both, initConnection,both,CONNECTION_LAYER_INVALID INPUT_FETCH_FAIL invokeAfterMsg,both, invokeAfterMsgs,both, invokeWithLayer,both,AUTH_BYTES_INVALID AUTH_KEY_DUPLICATED CDN_METHOD_INVALID CHAT_WRITE_FORBIDDEN CONNECTION_API_ID_INVALID CONNECTION_DEVICE_MODEL_EMPTY CONNECTION_LANG_PACK_INVALID CONNECTION_NOT_INITED CONNECTION_SYSTEM_EMPTY INPUT_LAYER_INVALID INVITE_HASH_EXPIRED NEED_MEMBER_INVALID TIMEOUT invokeWithMessagesRange,both, invokeWithTakeout,both, invokeWithoutUpdates,both, langpack.getDifference,user,LANG_PACK_INVALID langpack.getLangPack,user,LANG_PACK_INVALID langpack.getLanguage,user, langpack.getLanguages,user,LANG_PACK_INVALID langpack.getStrings,user,LANG_PACK_INVALID messages.acceptEncryption,user,CHAT_ID_INVALID ENCRYPTION_ALREADY_ACCEPTED ENCRYPTION_ALREADY_DECLINED ENCRYPTION_OCCUPY_FAILED messages.acceptUrlAuth,user, messages.addChatUser,user,CHAT_ADMIN_REQUIRED CHAT_ID_INVALID INPUT_USER_DEACTIVATED PEER_ID_INVALID USERS_TOO_MUCH USER_ALREADY_PARTICIPANT USER_ID_INVALID USER_NOT_MUTUAL_CONTACT USER_PRIVACY_RESTRICTED messages.checkChatInvite,user,INVITE_HASH_EMPTY INVITE_HASH_EXPIRED INVITE_HASH_INVALID messages.clearAllDrafts,user, messages.clearRecentStickers,user, messages.createChat,user,USERS_TOO_FEW USER_RESTRICTED messages.deleteChatUser,both,CHAT_ID_INVALID PEER_ID_INVALID USER_NOT_PARTICIPANT messages.deleteHistory,user,PEER_ID_INVALID messages.deleteMessages,both,MESSAGE_DELETE_FORBIDDEN messages.deleteScheduledMessages,user, messages.discardEncryption,user,CHAT_ID_EMPTY ENCRYPTION_ALREADY_DECLINED ENCRYPTION_ID_INVALID messages.editChatAbout,both, messages.editChatAdmin,user,CHAT_ID_INVALID messages.editChatDefaultBannedRights,both,BANNED_RIGHTS_INVALID messages.editChatPhoto,both,CHAT_ID_INVALID INPUT_CONSTRUCTOR_INVALID INPUT_FETCH_FAIL PEER_ID_INVALID PHOTO_EXT_INVALID messages.editChatTitle,both,CHAT_ID_INVALID NEED_CHAT_INVALID messages.editInlineBotMessage,both,ENTITY_BOUNDS_INVALID MESSAGE_ID_INVALID MESSAGE_NOT_MODIFIED messages.editMessage,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_WRITE_FORBIDDEN ENTITY_BOUNDS_INVALID INLINE_BOT_REQUIRED INPUT_USER_DEACTIVATED MEDIA_GROUPED_INVALID MEDIA_NEW_INVALID MEDIA_PREV_INVALID MESSAGE_AUTHOR_REQUIRED MESSAGE_EDIT_TIME_EXPIRED MESSAGE_EMPTY MESSAGE_ID_INVALID MESSAGE_NOT_MODIFIED PEER_ID_INVALID messages.exportChatInvite,both,CHAT_ID_INVALID EXPIRE_DATE_INVALID messages.faveSticker,user,STICKER_ID_INVALID messages.forwardMessages,both,BROADCAST_PUBLIC_VOTERS_FORBIDDEN CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_ID_INVALID CHAT_SEND_GIFS_FORBIDDEN CHAT_SEND_MEDIA_FORBIDDEN CHAT_SEND_STICKERS_FORBIDDEN CHAT_WRITE_FORBIDDEN GROUPED_MEDIA_INVALID INPUT_USER_DEACTIVATED MEDIA_EMPTY MESSAGE_IDS_EMPTY MESSAGE_ID_INVALID PEER_ID_INVALID PTS_CHANGE_EMPTY QUIZ_ANSWER_MISSING RANDOM_ID_DUPLICATE RANDOM_ID_INVALID SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH TIMEOUT TOPIC_DELETED USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT YOU_BLOCKED_USER messages.getAllChats,user, messages.getAllDrafts,user, messages.getAllStickers,user, messages.getArchivedStickers,user, messages.getAttachedStickers,user, messages.getBotCallbackAnswer,user,BOT_RESPONSE_TIMEOUT CHANNEL_INVALID DATA_INVALID MESSAGE_ID_INVALID PEER_ID_INVALID TIMEOUT messages.getChats,both,CHAT_ID_INVALID PEER_ID_INVALID messages.getCommonChats,user,USER_ID_INVALID messages.getDhConfig,user,RANDOM_LENGTH_INVALID messages.getDialogFilters,user, messages.getDialogUnreadMarks,user, messages.getDialogs,user,INPUT_CONSTRUCTOR_INVALID OFFSET_PEER_ID_INVALID SESSION_PASSWORD_NEEDED TIMEOUT messages.getDocumentByHash,both,SHA256_HASH_INVALID messages.getEmojiKeywords,user, messages.getEmojiKeywordsDifference,user, messages.getEmojiKeywordsLanguages,user, messages.getEmojiURL,user, messages.getFavedStickers,user, messages.getFeaturedStickers,user, messages.getFullChat,both,CHAT_ID_INVALID PEER_ID_INVALID messages.getGameHighScores,bot,PEER_ID_INVALID USER_BOT_REQUIRED messages.getHistory,user,AUTH_KEY_DUPLICATED AUTH_KEY_PERM_EMPTY CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ID_INVALID PEER_ID_INVALID TIMEOUT messages.getInlineBotResults,user,BOT_INLINE_DISABLED BOT_INVALID CHANNEL_PRIVATE TIMEOUT messages.getInlineGameHighScores,bot,MESSAGE_ID_INVALID USER_BOT_REQUIRED messages.getMaskStickers,user, messages.getMessageEditData,user,MESSAGE_AUTHOR_REQUIRED PEER_ID_INVALID messages.getMessages,both, messages.getMessagesReadParticipants,user,CHAT_TOO_BIG MESSAGE_ID_INVALID messages.getMessagesViews,user,CHANNEL_PRIVATE CHAT_ID_INVALID PEER_ID_INVALID messages.getOnlines,user, messages.getPeerDialogs,user,CHANNEL_PRIVATE PEER_ID_INVALID messages.getPeerSettings,user,CHANNEL_INVALID PEER_ID_INVALID messages.getPinnedDialogs,user, messages.getPollResults,user, messages.getPollVotes,user,BROADCAST_FORBIDDEN POLL_VOTE_REQUIRED messages.getRecentLocations,user, messages.getRecentStickers,user, messages.getSavedGifs,user, messages.getScheduledHistory,user, messages.getScheduledMessages,user, messages.getSearchCounters,user, messages.getSplitRanges,user, messages.getStatsURL,user, messages.getStickerSet,both,EMOTICON_STICKERPACK_MISSING STICKERSET_INVALID messages.getStickers,user,EMOTICON_EMPTY messages.getSuggestedDialogFilters,user, messages.getUnreadMentions,user,PEER_ID_INVALID messages.getWebPage,user,WC_CONVERT_URL_INVALID messages.getWebPagePreview,user,ENTITY_BOUNDS_INVALID messages.hideAllChatJoinRequests,user,HIDE_REQUESTER_MISSING messages.hidePeerSettingsBar,user, messages.importChatInvite,user,CHANNELS_TOO_MUCH INVITE_HASH_EMPTY INVITE_HASH_EXPIRED INVITE_HASH_INVALID INVITE_REQUEST_SENT SESSION_PASSWORD_NEEDED USERS_TOO_MUCH USER_ALREADY_PARTICIPANT messages.initHistoryImport,user,IMPORT_FILE_INVALID IMPORT_FORMAT_UNRECOGNIZED PREVIOUS_CHAT_IMPORT_ACTIVE_WAIT_XMIN TIMEOUT messages.installStickerSet,user,STICKERSET_INVALID messages.markDialogUnread,user, messages.migrateChat,user,CHAT_ADMIN_REQUIRED CHAT_ID_INVALID PEER_ID_INVALID messages.readEncryptedHistory,user,MSG_WAIT_FAILED messages.readFeaturedStickers,user, messages.readHistory,user,PEER_ID_INVALID TIMEOUT messages.readMentions,user, messages.readMessageContents,user, messages.receivedMessages,user, messages.receivedQueue,user,MAX_QTS_INVALID MSG_WAIT_FAILED messages.reorderPinnedDialogs,user,PEER_ID_INVALID messages.reorderStickerSets,user, messages.report,user, messages.reportEncryptedSpam,user,CHAT_ID_INVALID messages.reportSpam,user,PEER_ID_INVALID messages.requestEncryption,user,DH_G_A_INVALID USER_ID_INVALID messages.requestUrlAuth,user, messages.saveDraft,user,ENTITY_BOUNDS_INVALID PEER_ID_INVALID messages.saveGif,user,GIF_ID_INVALID messages.saveRecentSticker,user,STICKER_ID_INVALID messages.search,user,CHAT_ADMIN_REQUIRED FROM_PEER_INVALID INPUT_CONSTRUCTOR_INVALID INPUT_FILTER_INVALID INPUT_USER_DEACTIVATED PEER_ID_INVALID PEER_ID_NOT_SUPPORTED SEARCH_QUERY_EMPTY USER_ID_INVALID messages.searchGifs,user,METHOD_INVALID SEARCH_QUERY_EMPTY messages.searchGlobal,user,SEARCH_QUERY_EMPTY messages.searchStickerSets,user, messages.sendEncrypted,user,CHAT_ID_INVALID DATA_INVALID ENCRYPTION_DECLINED MSG_WAIT_FAILED messages.sendEncryptedFile,user,MSG_WAIT_FAILED messages.sendEncryptedService,user,DATA_INVALID ENCRYPTION_DECLINED MSG_WAIT_FAILED USER_IS_BLOCKED messages.sendInlineBotResult,user,CHAT_SEND_INLINE_FORBIDDEN CHAT_WRITE_FORBIDDEN ENTITY_BOUNDS_INVALID INLINE_RESULT_EXPIRED PEER_ID_INVALID QUERY_ID_EMPTY SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH TOPIC_DELETED WEBPAGE_CURL_FAILED WEBPAGE_MEDIA_EMPTY messages.sendMedia,both,BOT_PAYMENTS_DISABLED BOT_POLLS_DISABLED BROADCAST_PUBLIC_VOTERS_FORBIDDEN CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_SEND_MEDIA_FORBIDDEN CHAT_WRITE_FORBIDDEN CURRENCY_TOTAL_AMOUNT_INVALID EMOTICON_INVALID ENTITY_BOUNDS_INVALID EXTERNAL_URL_INVALID FILE_PARTS_INVALID FILE_PART_LENGTH_INVALID FILE_REFERENCE_EMPTY FILE_REFERENCE_EXPIRED GAME_BOT_INVALID INPUT_USER_DEACTIVATED MEDIA_CAPTION_TOO_LONG MEDIA_EMPTY PAYMENT_PROVIDER_INVALID PEER_ID_INVALID PHOTO_EXT_INVALID PHOTO_INVALID_DIMENSIONS PHOTO_SAVE_FILE_INVALID POLL_ANSWERS_INVALID POLL_OPTION_DUPLICATE POLL_QUESTION_INVALID POSTPONED_TIMEOUT QUIZ_CORRECT_ANSWERS_EMPTY QUIZ_CORRECT_ANSWERS_TOO_MUCH QUIZ_CORRECT_ANSWER_INVALID QUIZ_MULTIPLE_INVALID RANDOM_ID_DUPLICATE SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH STORAGE_CHECK_FAILED TIMEOUT TOPIC_DELETED USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT VIDEO_CONTENT_TYPE_INVALID WEBPAGE_CURL_FAILED WEBPAGE_MEDIA_EMPTY messages.sendMessage,both,AUTH_KEY_DUPLICATED BOT_DOMAIN_INVALID BUTTON_DATA_INVALID BUTTON_TYPE_INVALID BUTTON_URL_INVALID CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_ID_INVALID CHAT_RESTRICTED CHAT_WRITE_FORBIDDEN ENTITIES_TOO_LONG ENTITY_BOUNDS_INVALID ENTITY_MENTION_USER_INVALID INPUT_USER_DEACTIVATED MESSAGE_EMPTY MESSAGE_TOO_LONG MSG_ID_INVALID PEER_ID_INVALID POLL_OPTION_INVALID RANDOM_ID_DUPLICATE REPLY_MARKUP_INVALID REPLY_MARKUP_TOO_LONG SCHEDULE_BOT_NOT_ALLOWED SCHEDULE_DATE_TOO_LATE SCHEDULE_STATUS_PRIVATE SCHEDULE_TOO_MUCH TIMEOUT TOPIC_DELETED USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT YOU_BLOCKED_USER messages.sendMultiMedia,both,ENTITY_BOUNDS_INVALID MULTI_MEDIA_TOO_LONG SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH TOPIC_DELETED messages.sendScheduledMessages,user, messages.sendVote,user,MESSAGE_POLL_CLOSED OPTION_INVALID messages.setBotCallbackAnswer,both,QUERY_ID_INVALID URL_INVALID messages.setBotPrecheckoutResults,both,ERROR_TEXT_EMPTY messages.setBotShippingResults,both,QUERY_ID_INVALID messages.setChatTheme,user,EMOJI_INVALID EMOJI_NOT_MODIFIED PEER_ID_INVALID messages.setEncryptedTyping,user,CHAT_ID_INVALID messages.setGameScore,bot,PEER_ID_INVALID USER_BOT_REQUIRED messages.setHistoryTTL,user,CHAT_NOT_MODIFIED TTL_PERIOD_INVALID messages.setInlineBotResults,bot,ARTICLE_TITLE_EMPTY AUDIO_CONTENT_URL_EMPTY AUDIO_TITLE_EMPTY BUTTON_DATA_INVALID BUTTON_TYPE_INVALID BUTTON_URL_INVALID DOCUMENT_INVALID FILE_CONTENT_TYPE_INVALID FILE_TITLE_EMPTY GIF_CONTENT_TYPE_INVALID MESSAGE_EMPTY NEXT_OFFSET_INVALID PHOTO_CONTENT_TYPE_INVALID PHOTO_CONTENT_URL_EMPTY PHOTO_THUMB_URL_EMPTY QUERY_ID_INVALID REPLY_MARKUP_INVALID RESULT_TYPE_INVALID SEND_MESSAGE_MEDIA_INVALID SEND_MESSAGE_TYPE_INVALID START_PARAM_INVALID STICKER_DOCUMENT_INVALID USER_BOT_INVALID VIDEO_TITLE_EMPTY WEBDOCUMENT_MIME_INVALID WEBDOCUMENT_URL_INVALID messages.setInlineGameScore,bot,MESSAGE_ID_INVALID USER_BOT_REQUIRED messages.setTyping,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ID_INVALID CHAT_WRITE_FORBIDDEN PEER_ID_INVALID USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT messages.startBot,user,BOT_INVALID PEER_ID_INVALID START_PARAM_EMPTY START_PARAM_INVALID messages.startHistoryImport,user,IMPORT_ID_INVALID messages.toggleDialogPin,user,PEER_HISTORY_EMPTY PEER_ID_INVALID PINNED_DIALOGS_TOO_MUCH messages.toggleStickerSets,user, messages.uninstallStickerSet,user,STICKERSET_INVALID messages.updateDialogFilter,user, messages.updateDialogFiltersOrder,user, messages.updatePinnedMessage,both,BOT_ONESIDE_NOT_AVAIL messages.uploadEncryptedFile,user, messages.uploadMedia,both,BOT_MISSING MEDIA_INVALID PEER_ID_INVALID POSTPONED_TIMEOUT payments.clearSavedInfo,user, payments.getBankCardData,user,BANK_CARD_NUMBER_INVALID payments.getPaymentForm,user,MESSAGE_ID_INVALID payments.getPaymentReceipt,user,MESSAGE_ID_INVALID payments.getSavedInfo,user, payments.sendPaymentForm,user,MESSAGE_ID_INVALID payments.validateRequestedInfo,user,MESSAGE_ID_INVALID phone.acceptCall,user,CALL_ALREADY_ACCEPTED CALL_ALREADY_DECLINED CALL_OCCUPY_FAILED CALL_PEER_INVALID CALL_PROTOCOL_FLAGS_INVALID phone.confirmCall,user,CALL_ALREADY_DECLINED CALL_PEER_INVALID phone.createGroupCall,user,SCHEDULE_DATE_INVALID phone.discardCall,user,CALL_ALREADY_ACCEPTED CALL_PEER_INVALID phone.discardGroupCallRequest,user,GROUPCALL_ALREADY_DISCARDED phone.editGroupCallParticipant,user,USER_VOLUME_INVALID phone.getCallConfig,user, phone.inviteToGroupCall,user,GROUPCALL_FORBIDDEN INVITE_FORBIDDEN_WITH_JOINAS USER_ALREADY_INVITED phone.joinGroupCall,user,GROUPCALL_ADD_PARTICIPANTS_FAILED GROUPCALL_SSRC_DUPLICATE_MUCH phone.joinGroupCallPresentation,user,PARTICIPANT_JOIN_MISSING phone.receivedCall,user,CALL_ALREADY_DECLINED CALL_PEER_INVALID phone.requestCall,user,CALL_PROTOCOL_FLAGS_INVALID PARTICIPANT_CALL_FAILED PARTICIPANT_VERSION_OUTDATED USER_ID_INVALID USER_IS_BLOCKED USER_PRIVACY_RESTRICTED phone.saveCallDebug,user,CALL_PEER_INVALID DATA_JSON_INVALID phone.setCallRating,user,CALL_PEER_INVALID phone.toggleGroupCallSettings,user,GROUPCALL_NOT_MODIFIED photos.deletePhotos,user, photos.getUserPhotos,both,MAX_ID_INVALID USER_ID_INVALID photos.updateProfilePhoto,user,PHOTO_ID_INVALID photos.uploadProfilePhoto,user,ALBUM_PHOTOS_TOO_MANY FILE_PARTS_INVALID IMAGE_PROCESS_FAILED PHOTO_CROP_SIZE_SMALL PHOTO_EXT_INVALID STICKER_MIME_INVALID VIDEO_FILE_INVALID ping,both, reqDHParams,both, reqPq,both, reqPqMulti,both, rpcDropAnswer,both, setClientDHParams,both, stats.getBroadcastStats,user,BROADCAST_REQUIRED CHAT_ADMIN_REQUIRED CHP_CALL_FAIL STATS_MIGRATE_X stats.getMegagroupStats,user,CHAT_ADMIN_REQUIRED MEGAGROUP_REQUIRED STATS_MIGRATE_X stats.loadAsyncGraph,user,GRAPH_INVALID_RELOAD GRAPH_OUTDATED_RELOAD stickers.addStickerToSet,both,BOT_MISSING STICKERSET_INVALID STICKER_PNG_NOPNG STICKER_TGS_NOTGS stickers.changeStickerPosition,bot,BOT_MISSING STICKER_INVALID stickers.checkShortName,user,SHORT_NAME_INVALID SHORT_NAME_OCCUPIED stickers.createStickerSet,both,BOT_MISSING PACK_SHORT_NAME_INVALID PACK_SHORT_NAME_OCCUPIED PEER_ID_INVALID SHORTNAME_OCCUPY_FAILED STICKERS_EMPTY STICKER_EMOJI_INVALID STICKER_FILE_INVALID STICKER_PNG_DIMENSIONS STICKER_PNG_NOPNG STICKER_TGS_NOTGS STICKER_THUMB_PNG_NOPNG STICKER_THUMB_TGS_NOTGS USER_ID_INVALID stickers.removeStickerFromSet,bot,BOT_MISSING STICKER_INVALID stickers.setStickerSetThumb,bot,STICKER_THUMB_PNG_NOPNG STICKER_THUMB_TGS_NOTGS stickers.suggestShortName,user,TITLE_INVALID updates.getChannelDifference,both,CHANNEL_INVALID CHANNEL_PRIVATE CHANNEL_PUBLIC_GROUP_NA HISTORY_GET_FAILED PERSISTENT_TIMESTAMP_EMPTY PERSISTENT_TIMESTAMP_INVALID PERSISTENT_TIMESTAMP_OUTDATED RANGES_INVALID TIMEOUT updates.getDifference,both,AUTH_KEY_PERM_EMPTY CDN_METHOD_INVALID DATE_EMPTY NEED_MEMBER_INVALID PERSISTENT_TIMESTAMP_EMPTY PERSISTENT_TIMESTAMP_INVALID SESSION_PASSWORD_NEEDED STORE_INVALID_SCALAR_TYPE TIMEOUT updates.getState,both,AUTH_KEY_DUPLICATED MSGID_DECREASE_RETRY SESSION_PASSWORD_NEEDED TIMEOUT upload.getCdnFile,user,UNKNOWN_METHOD upload.getCdnFileHashes,both,CDN_METHOD_INVALID RSA_DECRYPT_FAILED upload.getFile,both,AUTH_KEY_PERM_EMPTY FILE_ID_INVALID INPUT_FETCH_FAIL LIMIT_INVALID LOCATION_INVALID OFFSET_INVALID TIMEOUT upload.getFileHashes,both, upload.getWebFile,user,LOCATION_INVALID upload.reuploadCdnFile,both,RSA_DECRYPT_FAILED upload.saveBigFilePart,both,FILE_PARTS_INVALID FILE_PART_EMPTY FILE_PART_INVALID FILE_PART_SIZE_CHANGED FILE_PART_SIZE_INVALID TIMEOUT upload.saveFilePart,both,FILE_PART_EMPTY FILE_PART_INVALID INPUT_FETCH_FAIL SESSION_PASSWORD_NEEDED users.getFullUser,both,TIMEOUT USER_ID_INVALID users.getUsers,both,AUTH_KEY_PERM_EMPTY MEMBER_NO_LOCATION NEED_MEMBER_INVALID SESSION_PASSWORD_NEEDED TIMEOUT users.setSecureValueErrors,bot, Telethon-1.39.0/telethon_generator/data/mtproto.tl000066400000000000000000000124471475566265000222640ustar00rootroot00000000000000// Core types (no need to gen) //vector#1cb5c415 {t:Type} # [ t ] = Vector t; /////////////////////////////// /// Authorization key creation /////////////////////////////// resPQ#05162463 nonce:int128 server_nonce:int128 pq:string server_public_key_fingerprints:Vector = ResPQ; p_q_inner_data#83c95aec pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 = P_Q_inner_data; p_q_inner_data_dc#a9f55f95 pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 dc:int = P_Q_inner_data; p_q_inner_data_temp#3c6a84d4 pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 expires_in:int = P_Q_inner_data; p_q_inner_data_temp_dc#56fddf88 pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 dc:int expires_in:int = P_Q_inner_data; bind_auth_key_inner#75a3f765 nonce:long temp_auth_key_id:long perm_auth_key_id:long temp_session_id:long expires_at:int = BindAuthKeyInner; server_DH_params_fail#79cb045d nonce:int128 server_nonce:int128 new_nonce_hash:int128 = Server_DH_Params; server_DH_params_ok#d0e8075c nonce:int128 server_nonce:int128 encrypted_answer:string = Server_DH_Params; server_DH_inner_data#b5890dba nonce:int128 server_nonce:int128 g:int dh_prime:string g_a:string server_time:int = Server_DH_inner_data; client_DH_inner_data#6643b654 nonce:int128 server_nonce:int128 retry_id:long g_b:string = Client_DH_Inner_Data; dh_gen_ok#3bcbf734 nonce:int128 server_nonce:int128 new_nonce_hash1:int128 = Set_client_DH_params_answer; dh_gen_retry#46dc1fb9 nonce:int128 server_nonce:int128 new_nonce_hash2:int128 = Set_client_DH_params_answer; dh_gen_fail#a69dae02 nonce:int128 server_nonce:int128 new_nonce_hash3:int128 = Set_client_DH_params_answer; destroy_auth_key_ok#f660e1d4 = DestroyAuthKeyRes; destroy_auth_key_none#0a9f2259 = DestroyAuthKeyRes; destroy_auth_key_fail#ea109b13 = DestroyAuthKeyRes; ---functions--- req_pq#60469778 nonce:int128 = ResPQ; req_pq_multi#be7e8ef1 nonce:int128 = ResPQ; req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:string q:string public_key_fingerprint:long encrypted_data:string = Server_DH_Params; set_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:string = Set_client_DH_params_answer; destroy_auth_key#d1435160 = DestroyAuthKeyRes; /////////////////////////////// ////////////// System messages /////////////////////////////// ---types--- msgs_ack#62d6b459 msg_ids:Vector = MsgsAck; bad_msg_notification#a7eff811 bad_msg_id:long bad_msg_seqno:int error_code:int = BadMsgNotification; bad_server_salt#edab447b bad_msg_id:long bad_msg_seqno:int error_code:int new_server_salt:long = BadMsgNotification; msgs_state_req#da69fb52 msg_ids:Vector = MsgsStateReq; msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo; msgs_all_info#8cc0d131 msg_ids:Vector info:string = MsgsAllInfo; msg_detailed_info#276d3ec6 msg_id:long answer_msg_id:long bytes:int status:int = MsgDetailedInfo; msg_new_detailed_info#809db6df answer_msg_id:long bytes:int status:int = MsgDetailedInfo; msg_resend_req#7d861a08 msg_ids:Vector = MsgResendReq; //rpc_result#f35c6d01 req_msg_id:long result:Object = RpcResult; // parsed manually rpc_error#2144ca19 error_code:int error_message:string = RpcError; rpc_answer_unknown#5e2ad36e = RpcDropAnswer; rpc_answer_dropped_running#cd78e586 = RpcDropAnswer; rpc_answer_dropped#a43ad8b7 msg_id:long seq_no:int bytes:int = RpcDropAnswer; future_salt#0949d9dc valid_since:int valid_until:int salt:long = FutureSalt; future_salts#ae500895 req_msg_id:long now:int salts:vector = FutureSalts; pong#347773c5 msg_id:long ping_id:long = Pong; destroy_session_ok#e22045fc session_id:long = DestroySessionRes; destroy_session_none#62d350c9 session_id:long = DestroySessionRes; new_session_created#9ec20908 first_msg_id:long unique_id:long server_salt:long = NewSession; //message msg_id:long seqno:int bytes:int body:Object = Message; // parsed manually //msg_container#73f1f8dc messages:vector = MessageContainer; // parsed manually //msg_copy#e06046b2 orig_message:Message = MessageCopy; // parsed manually, not used - use msg_container //gzip_packed#3072cfa1 packed_data:string = Object; // parsed manually http_wait#9299359f max_delay:int wait_after:int max_wait:int = HttpWait; //ipPort ipv4:int port:int = IpPort; //help.configSimple#d997c3c5 date:int expires:int dc_id:int ip_port_list:Vector = help.ConfigSimple; ipPort#d433ad73 ipv4:int port:int = IpPort; ipPortSecret#37982646 ipv4:int port:int secret:bytes = IpPort; accessPointRule#4679b65f phone_prefix_rules:string dc_id:int ips:vector = AccessPointRule; help.configSimple#5a592a6c date:int expires:int rules:vector = help.ConfigSimple; tlsClientHello blocks:vector = TlsClientHello; tlsBlockString data:string = TlsBlock; tlsBlockRandom length:int = TlsBlock; tlsBlockZero length:int = TlsBlock; tlsBlockDomain = TlsBlock; tlsBlockGrease seed:int = TlsBlock; tlsBlockPublicKey = TlsBlock; tlsBlockScope entries:Vector = TlsBlock; ---functions--- rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer; get_future_salts#b921bd04 num:int = FutureSalts; ping#7abe77ec ping_id:long = Pong; ping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong; destroy_session#e7512126 session_id:long = DestroySessionRes; Telethon-1.39.0/telethon_generator/docswriter.py000066400000000000000000000227571475566265000220520ustar00rootroot00000000000000import os import re class DocsWriter: """ Utility class used to write the HTML files used on the documentation. """ def __init__(self, filename, type_to_path): """ Initializes the writer to the specified output file, creating the parent directories when used if required. """ self.filename = filename self._parent = str(self.filename.parent) self.handle = None self.title = '' # Should be set before calling adding items to the menu self.menu_separator_tag = None # Utility functions self.type_to_path = lambda t: self._rel(type_to_path(t)) # Control signals self.menu_began = False self.table_columns = 0 self.table_columns_left = None self.write_copy_script = False self._script = '' def _rel(self, path): """ Get the relative path for the given path from the current file by working around https://bugs.python.org/issue20012. """ return os.path.relpath( str(path), self._parent).replace(os.path.sep, '/') # High level writing def write_head(self, title, css_path, default_css): """Writes the head part for the generated document, with the given title and CSS """ self.title = title self.write( ''' {title}
    ''', title=title, rel_css=self._rel(css_path), def_css=default_css ) def set_menu_separator(self, img): """Sets the menu separator. Must be called before adding entries to the menu """ if img: self.menu_separator_tag = '/'.format( self._rel(img)) else: self.menu_separator_tag = None def add_menu(self, name, link=None): """Adds a menu entry, will create it if it doesn't exist yet""" if self.menu_began: if self.menu_separator_tag: self.write(self.menu_separator_tag) else: # First time, create the menu tag self.write('') def write_title(self, title, level=1, id=None): """Writes a title header in the document body, with an optional depth level """ if id: self.write('{title}', title=title, lv=level, id=id) else: self.write('{title}', title=title, lv=level) def write_code(self, tlobject): """Writes the code for the given 'tlobject' properly formatted with hyperlinks """ self.write('
    ---{}---\n',
                       'functions' if tlobject.is_function else 'types')
    
            # Write the function or type and its ID
            if tlobject.namespace:
                self.write(tlobject.namespace)
                self.write('.')
    
            self.write('{}#{:08x}', tlobject.name, tlobject.id)
    
            # Write all the arguments (or do nothing if there's none)
            for arg in tlobject.args:
                self.write(' ')
                add_link = not arg.generic_definition and not arg.is_generic
    
                # "Opening" modifiers
                if arg.generic_definition:
                    self.write('{')
    
                # Argument name
                self.write(arg.name)
                self.write(':')
    
                # "Opening" modifiers
                if arg.flag:
                    self.write('{}.{}?', arg.flag, arg.flag_index)
    
                if arg.is_generic:
                    self.write('!')
    
                if arg.is_vector:
                    self.write('Vector<',
                               self.type_to_path('vector'))
    
                # Argument type
                if arg.type:
                    if add_link:
                        self.write('', self.type_to_path(arg.type))
                    self.write(arg.type)
                    if add_link:
                        self.write('')
                else:
                    self.write('#')
    
                # "Closing" modifiers
                if arg.is_vector:
                    self.write('>')
    
                if arg.generic_definition:
                    self.write('}')
    
            # Now write the resulting type (result from a function/type)
            self.write(' = ')
            generic_name = next((arg.name for arg in tlobject.args
                                 if arg.generic_definition), None)
    
            if tlobject.result == generic_name:
                # Generic results cannot have any link
                self.write(tlobject.result)
            else:
                if re.search('^vector<', tlobject.result, re.IGNORECASE):
                    # Notice that we don't simply make up the "Vector" part,
                    # because some requests (as of now, only FutureSalts),
                    # use a lower type name for it (see #81)
                    vector, inner = tlobject.result.split('<')
                    inner = inner.strip('>')
                    self.write('{}<',
                               self.type_to_path(vector), vector)
    
                    self.write('{}>',
                               self.type_to_path(inner), inner)
                else:
                    self.write('{}',
                               self.type_to_path(tlobject.result), tlobject.result)
    
            self.write('
    ') def begin_table(self, column_count): """Begins a table with the given 'column_count', required to automatically create the right amount of columns when adding items to the rows""" self.table_columns = column_count self.table_columns_left = 0 self.write('') def add_row(self, text, link=None, bold=False, align=None): """This will create a new row, or add text to the next column of the previously created, incomplete row, closing it if complete""" if not self.table_columns_left: # Starting a new row self.write('') self.table_columns_left = self.table_columns self.write('') if bold: self.write('') if link: self.write('', self._rel(link)) # Finally write the real table data, the given text self.write(text) if link: self.write('') if bold: self.write('') self.write('') self.table_columns_left -= 1 if not self.table_columns_left: self.write('') def end_table(self): # If there was any column left, finish it before closing the table if self.table_columns_left: self.write('') self.write('
    ') def write_text(self, text): """Writes a paragraph of text""" self.write('

    {}

    ', text) def write_copy_button(self, text, text_to_copy): """Writes a button with 'text' which can be used to copy 'text_to_copy' to clipboard when it's clicked.""" self.write_copy_script = True self.write('' .format(text_to_copy, text)) def add_script(self, src='', path=None): if path: self._script += ''.format( self._rel(path)) elif src: self._script += ''.format(src) def end_body(self): """Ends the whole document. This should be called the last""" if self.write_copy_script: self.write( '' '' ) self.write('
    {}', self._script) # "Low" level writing def write(self, s, *args, **kwargs): """Wrapper around handle.write""" if args or kwargs: self.handle.write(s.format(*args, **kwargs)) else: self.handle.write(s) # With block def __enter__(self): # Sanity check self.filename.parent.mkdir(parents=True, exist_ok=True) self.handle = self.filename.open('w', encoding='utf-8') return self def __exit__(self, exc_type, exc_val, exc_tb): self.handle.close() Telethon-1.39.0/telethon_generator/generators/000077500000000000000000000000001475566265000214475ustar00rootroot00000000000000Telethon-1.39.0/telethon_generator/generators/__init__.py000066400000000000000000000001761475566265000235640ustar00rootroot00000000000000from .errors import generate_errors from .tlobject import generate_tlobjects, clean_tlobjects from .docs import generate_docs Telethon-1.39.0/telethon_generator/generators/docs.py000077500000000000000000000604221475566265000227600ustar00rootroot00000000000000#!/usr/bin/env python3 import functools import os import pathlib import re import shutil from collections import defaultdict from pathlib import Path from ..docswriter import DocsWriter from ..parsers import TLObject, Usability from ..utils import snake_to_camel_case CORE_TYPES = { 'int', 'long', 'int128', 'int256', 'double', 'vector', 'string', 'bool', 'true', 'bytes', 'date' } def _get_file_name(tlobject): """``ClassName -> class_name.html``.""" name = tlobject.name if isinstance(tlobject, TLObject) else tlobject # Courtesy of http://stackoverflow.com/a/1176023/4759433 s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) result = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() return '{}.html'.format(result) def get_import_code(tlobject): """``TLObject -> from ... import ...``.""" kind = 'functions' if tlobject.is_function else 'types' ns = '.' + tlobject.namespace if tlobject.namespace else '' return 'from telethon.tl.{}{} import {}'\ .format(kind, ns, tlobject.class_name) def _get_path_for(tlobject): """Returns the path for the given TLObject.""" out_dir = pathlib.Path('methods' if tlobject.is_function else 'constructors') if tlobject.namespace: out_dir /= tlobject.namespace return out_dir / _get_file_name(tlobject) def _get_path_for_type(type_): """Similar to `_get_path_for` but for only type names.""" if type_.lower() in CORE_TYPES: return Path('index.html#%s' % type_.lower()) elif '.' in type_: namespace, name = type_.split('.') return Path('types', namespace, _get_file_name(name)) else: return Path('types', _get_file_name(type_)) def _find_title(html_file): """Finds the for the given HTML file, or (Unknown).""" # TODO Is it necessary to read files like this? with html_file.open() as f: for line in f: if '<title>' in line: # + 7 to skip len('<title>') return line[line.index('<title>') + 7:line.index('')] return '(Unknown)' def _build_menu(docs): """ Builds the menu used for the current ``DocumentWriter``. """ paths = [] current = docs.filename top = pathlib.Path('.') while current != top: current = current.parent paths.append(current) for path in reversed(paths): docs.add_menu(path.stem.title() or 'API', link=path / 'index.html') if docs.filename.stem != 'index': docs.add_menu(docs.title, link=docs.filename) docs.end_menu() def _generate_index(folder, paths, bots_index=False, bots_index_paths=()): """Generates the index file for the specified folder""" # Determine the namespaces listed here (as sub folders) # and the files (.html files) that we should link to namespaces = [] files = [] INDEX = 'index.html' BOT_INDEX = 'botindex.html' for item in (bots_index_paths or folder.iterdir()): if item.is_dir(): namespaces.append(item) elif item.name not in (INDEX, BOT_INDEX): files.append(item) # Now that everything is setup, write the index.html file filename = folder / (BOT_INDEX if bots_index else INDEX) with DocsWriter(filename, _get_path_for_type) as docs: # Title should be the current folder name docs.write_head(str(folder).replace(os.path.sep, '/').title(), css_path=paths['css'], default_css=paths['default_css']) docs.set_menu_separator(paths['arrow']) _build_menu(docs) docs.write_title(str(filename.parent) .replace(os.path.sep, '/').title()) if bots_index: docs.write_text('These are the requests that you may be able to ' 'use as a bot. Click here to ' 'view them all.'.format(INDEX)) else: docs.write_text('Click here to view the requests ' 'that you can use as a bot.'.format(BOT_INDEX)) if namespaces: docs.write_title('Namespaces', level=3) docs.begin_table(4) namespaces.sort() for namespace in namespaces: # For every namespace, also write the index of it namespace_paths = [] if bots_index: for item in bots_index_paths: if item.parent == namespace: namespace_paths.append(item) _generate_index(namespace, paths, bots_index, namespace_paths) docs.add_row( namespace.stem.title(), link=namespace / (BOT_INDEX if bots_index else INDEX)) docs.end_table() docs.write_title('Available items') docs.begin_table(2) files = [(f, _find_title(f)) for f in files] files.sort(key=lambda t: t[1]) for file, title in files: docs.add_row(title, link=file) docs.end_table() docs.end_body() def _get_description(arg): """Generates a proper description for the given argument.""" desc = [] otherwise = False if arg.can_be_inferred: desc.append('If left unspecified, it will be inferred automatically.') otherwise = True elif arg.flag: desc.append('This argument defaults to ' 'None and can be omitted.') otherwise = True if arg.type in {'InputPeer', 'InputUser', 'InputChannel', 'InputNotifyPeer', 'InputDialogPeer'}: desc.append( 'Anything entity-like will work if the library can find its ' 'Input version (e.g., usernames, Peer, ' 'User or Channel objects, etc.).' ) if arg.is_vector: if arg.is_generic: desc.append('A list of other Requests must be supplied.') else: desc.append('A list must be supplied.') elif arg.is_generic: desc.append('A different Request must be supplied for this argument.') else: otherwise = False # Always reset to false if no other text is added if otherwise: desc.insert(1, 'Otherwise,') desc[-1] = desc[-1][:1].lower() + desc[-1][1:] return ' '.join(desc).replace( 'list', 'list' ) def _copy_replace(src, dst, replacements): """Copies the src file into dst applying the replacements dict""" with src.open() as infile, dst.open('w') as outfile: outfile.write(re.sub( '|'.join(re.escape(k) for k in replacements), lambda m: str(replacements[m.group(0)]), infile.read() )) def _write_html_pages(tlobjects, methods, layer, input_res): """ Generates the documentation HTML files from from ``scheme.tl`` to ``/methods`` and ``/constructors``, etc. """ # Save 'Type: [Constructors]' for use in both: # * Seeing the return type or constructors belonging to the same type. # * Generating the types documentation, showing available constructors. paths = {k: pathlib.Path(v) for k, v in ( ('css', 'css'), ('arrow', 'img/arrow.svg'), ('search.js', 'js/search.js'), ('404', '404.html'), ('index_all', 'index.html'), ('bot_index', 'botindex.html'), ('index_types', 'types/index.html'), ('index_methods', 'methods/index.html'), ('index_constructors', 'constructors/index.html') )} paths['default_css'] = 'light' # docs..css, local path type_to_constructors = defaultdict(list) type_to_functions = defaultdict(list) for tlobject in tlobjects: d = type_to_functions if tlobject.is_function else type_to_constructors d[tlobject.innermost_result].append(tlobject) for t, cs in type_to_constructors.items(): type_to_constructors[t] = list(sorted(cs, key=lambda c: c.name)) methods = {m.name: m for m in methods} bot_docs_paths = [] for tlobject in tlobjects: filename = _get_path_for(tlobject) with DocsWriter(filename, _get_path_for_type) as docs: docs.write_head(title=tlobject.class_name, css_path=paths['css'], default_css=paths['default_css']) # Create the menu (path to the current TLObject) docs.set_menu_separator(paths['arrow']) _build_menu(docs) # Create the page title docs.write_title(tlobject.class_name) if tlobject.is_function: if tlobject.usability == Usability.USER: start = 'Only users can' elif tlobject.usability == Usability.BOT: bot_docs_paths.append(filename) start = 'Only bots can' elif tlobject.usability == Usability.BOTH: bot_docs_paths.append(filename) start = 'Both users and bots can' else: bot_docs_paths.append(filename) start = \ 'Both users and bots may be able to' docs.write_text('{} use this request. ' 'See code examples.'.format(start)) # Write the code definition for this TLObject docs.write_code(tlobject) docs.write_copy_button('Copy import to the clipboard', get_import_code(tlobject)) # Write the return type (or constructors belonging to the same type) docs.write_title('Returns' if tlobject.is_function else 'Belongs to', level=3) generic_arg = next((arg.name for arg in tlobject.args if arg.generic_definition), None) if tlobject.result == generic_arg: # We assume it's a function returning a generic type generic_arg = next((arg.name for arg in tlobject.args if arg.is_generic)) docs.write_text('This request returns the result of whatever ' 'the result from invoking the request passed ' 'through {} is.'.format(generic_arg)) else: if re.search('^vector<', tlobject.result, re.IGNORECASE): docs.write_text('A list of the following type is returned.') inner = tlobject.innermost_result else: inner = tlobject.result docs.begin_table(column_count=1) docs.add_row(inner, link=_get_path_for_type(inner)) docs.end_table() cs = type_to_constructors.get(inner, []) if not cs: docs.write_text('This type has no instances available.') elif len(cs) == 1: docs.write_text('This type can only be an instance of:') else: docs.write_text('This type can be an instance of either:') docs.begin_table(column_count=2) for constructor in cs: link = _get_path_for(constructor) docs.add_row(constructor.class_name, link=link) docs.end_table() # Return (or similar types) written. Now parameters/members docs.write_title( 'Parameters' if tlobject.is_function else 'Members', level=3 ) # Sort the arguments in the same way they're sorted # on the generated code (flags go last) args = [ a for a in tlobject.sorted_args() if not a.flag_indicator and not a.generic_definition ] if args: # Writing parameters docs.begin_table(column_count=3) for arg in args: # Name row docs.add_row(arg.name, bold=True) # Type row friendly_type = 'flag' if arg.type == 'true' else arg.type if arg.is_generic: docs.add_row('!' + friendly_type, align='center') else: docs.add_row( friendly_type, align='center', link=_get_path_for_type(arg.type) ) # Add a description for this argument docs.add_row(_get_description(arg)) docs.end_table() else: if tlobject.is_function: docs.write_text('This request takes no input parameters.') else: docs.write_text('This type has no members.') if tlobject.is_function: docs.write_title('Known RPC errors') method_info = methods.get(tlobject.fullname) errors = method_info and method_info.errors if not errors: docs.write_text("This request can't cause any RPC error " "as far as we know.") else: docs.write_text( 'This request can cause {} known error{}:'.format( len(errors), '' if len(errors) == 1 else 's' )) docs.begin_table(column_count=2) for error in errors: docs.add_row('{}'.format(error.name)) docs.add_row('{}.'.format(error.description)) docs.end_table() docs.write_text('You can import these from ' 'telethon.errors.') docs.write_title('Example', id='examples') if tlobject.friendly: ns, friendly = tlobject.friendly docs.write_text( 'Please refer to the documentation of client.{1}() ' 'to learn about the parameters and see several code ' 'examples on how to use it.' .format(ns, friendly) ) docs.write_text( 'The method above is the recommended way to do it. ' 'If you need more control over the parameters or want ' 'to learn how it is implemented, open the details by ' 'clicking on the "Details" text.' ) docs.write('
    ') docs.write('''
    \
    from telethon.sync import TelegramClient
    from telethon import functions, types
    
    with TelegramClient(name, api_id, api_hash) as client:
        result = client(''')
                    tlobject.as_example(docs, indent=1)
                    docs.write(')\n')
                    if tlobject.result.startswith('Vector'):
                        docs.write('''\
        for x in result:
            print(x''')
                    else:
                        docs.write('    print(result')
                        if tlobject.result != 'Bool' \
                                and not tlobject.result.startswith('Vector'):
                            docs.write('.stringify()')
    
                    docs.write(')
    ') if tlobject.friendly: docs.write('
    ') depth = '../' * (2 if tlobject.namespace else 1) docs.add_script(src='prependPath = "{}";'.format(depth)) docs.add_script(path=paths['search.js']) docs.end_body() # Find all the available types (which are not the same as the constructors) # Each type has a list of constructors associated to it, hence is a map for t, cs in type_to_constructors.items(): filename = _get_path_for_type(t) out_dir = filename.parent if out_dir: out_dir.mkdir(parents=True, exist_ok=True) # Since we don't have access to the full TLObject, split the type if '.' in t: namespace, name = t.split('.') else: namespace, name = None, t with DocsWriter(filename, _get_path_for_type) as docs: docs.write_head(title=snake_to_camel_case(name), css_path=paths['css'], default_css=paths['default_css']) docs.set_menu_separator(paths['arrow']) _build_menu(docs) # Main file title docs.write_title(snake_to_camel_case(name)) # List available constructors for this type docs.write_title('Available constructors', level=3) if not cs: docs.write_text('This type has no constructors available.') elif len(cs) == 1: docs.write_text('This type has one constructor available.') else: docs.write_text('This type has %d constructors available.' % len(cs)) docs.begin_table(2) for constructor in cs: # Constructor full name link = _get_path_for(constructor) docs.add_row(constructor.class_name, link=link) docs.end_table() # List all the methods which return this type docs.write_title('Requests returning this type', level=3) functions = type_to_functions.get(t, []) if not functions: docs.write_text('No request returns this type.') elif len(functions) == 1: docs.write_text('Only the following request returns this type.') else: docs.write_text( 'The following %d requests return this type as a result.' % len(functions) ) docs.begin_table(2) for func in functions: link = _get_path_for(func) docs.add_row(func.class_name, link=link) docs.end_table() # List all the methods which take this type as input docs.write_title('Requests accepting this type as input', level=3) other_methods = sorted( (u for u in tlobjects if any(a.type == t for a in u.args) and u.is_function), key=lambda u: u.name ) if not other_methods: docs.write_text( 'No request accepts this type as an input parameter.') elif len(other_methods) == 1: docs.write_text( 'Only this request has a parameter with this type.') else: docs.write_text( 'The following %d requests accept this type as an input ' 'parameter.' % len(other_methods)) docs.begin_table(2) for ot in other_methods: link = _get_path_for(ot) docs.add_row(ot.class_name, link=link) docs.end_table() # List every other type which has this type as a member docs.write_title('Other types containing this type', level=3) other_types = sorted( (u for u in tlobjects if any(a.type == t for a in u.args) and not u.is_function), key=lambda u: u.name ) if not other_types: docs.write_text( 'No other types have a member of this type.') elif len(other_types) == 1: docs.write_text( 'You can find this type as a member of this other type.') else: docs.write_text( 'You can find this type as a member of any of ' 'the following %d types.' % len(other_types)) docs.begin_table(2) for ot in other_types: link = _get_path_for(ot) docs.add_row(ot.class_name, link=link) docs.end_table() docs.end_body() # After everything's been written, generate an index.html per folder. # This will be done automatically and not taking into account any extra # information that we have available, simply a file listing all the others # accessible by clicking on their title for folder in ['types', 'methods', 'constructors']: _generate_index(pathlib.Path(folder), paths) _generate_index(pathlib.Path('methods'), paths, True, bot_docs_paths) # Write the final core index, the main index for the rest of files types = set() methods = [] cs = [] for tlobject in tlobjects: if tlobject.is_function: methods.append(tlobject) else: cs.append(tlobject) if not tlobject.result.lower() in CORE_TYPES: if re.search('^vector<', tlobject.result, re.IGNORECASE): types.add(tlobject.innermost_result) else: types.add(tlobject.result) types = sorted(types) methods = sorted(methods, key=lambda m: m.name) cs = sorted(cs, key=lambda c: c.name) shutil.copy(str(input_res / '404.html'), str(paths['404'])) _copy_replace(input_res / 'core.html', paths['index_all'], { '{type_count}': len(types), '{method_count}': len(methods), '{constructor_count}': len(tlobjects) - len(methods), '{layer}': layer, }) def fmt(xs): zs = {} # create a dict to hold those which have duplicated keys for x in xs: zs[x.class_name] = x.class_name in zs return ', '.join( '"{}.{}"'.format(x.namespace, x.class_name) if zs[x.class_name] and x.namespace else '"{}"'.format(x.class_name) for x in xs ) request_names = fmt(methods) constructor_names = fmt(cs) def fmt(xs, formatter): return ', '.join('"{}"'.format( formatter(x)).replace(os.path.sep, '/') for x in xs) type_names = fmt(types, formatter=lambda x: x) request_urls = fmt(methods, _get_path_for) type_urls = fmt(types, _get_path_for_type) constructor_urls = fmt(cs, _get_path_for) paths['search.js'].parent.mkdir(parents=True, exist_ok=True) _copy_replace(input_res / 'js/search.js', paths['search.js'], { '{request_names}': request_names, '{type_names}': type_names, '{constructor_names}': constructor_names, '{request_urls}': request_urls, '{type_urls}': type_urls, '{constructor_urls}': constructor_urls }) def _copy_resources(res_dir): for dirname, files in [('css', ['docs.light.css', 'docs.dark.css']), ('img', ['arrow.svg'])]: dirpath = pathlib.Path(dirname) dirpath.mkdir(parents=True, exist_ok=True) for file in files: shutil.copy(str(res_dir / dirname / file), str(dirpath)) def _create_structure(tlobjects): """ Pre-create the required directory structure. """ types_ns = set() method_ns = set() for obj in tlobjects: if obj.namespace: if obj.is_function: method_ns.add(obj.namespace) else: types_ns.add(obj.namespace) output_dir = pathlib.Path('.') type_dir = output_dir / 'types' type_dir.mkdir(exist_ok=True) cons_dir = output_dir / 'constructors' cons_dir.mkdir(exist_ok=True) for ns in types_ns: (type_dir / ns).mkdir(exist_ok=True) (cons_dir / ns).mkdir(exist_ok=True) meth_dir = output_dir / 'methods' meth_dir.mkdir(exist_ok=True) for ns in types_ns: (meth_dir / ns).mkdir(exist_ok=True) def generate_docs(tlobjects, methods_info, layer, input_res): _create_structure(tlobjects) _write_html_pages(tlobjects, methods_info, layer, input_res) _copy_resources(input_res) Telethon-1.39.0/telethon_generator/generators/errors.py000066400000000000000000000043671475566265000233470ustar00rootroot00000000000000def generate_errors(errors, f): # Exact/regex match to create {CODE: ErrorClassName} exact_match = [] regex_match = [] # Find out what subclasses to import and which to create import_base, create_base = set(), {} for error in errors: if error.subclass_exists: import_base.add(error.subclass) else: create_base[error.subclass] = error.int_code if error.has_captures: regex_match.append(error) else: exact_match.append(error) # Imports and new subclass creation f.write('from .rpcbaseerrors import RPCError, {}\n' .format(", ".join(sorted(import_base)))) for cls, int_code in sorted(create_base.items(), key=lambda t: t[1]): f.write('\n\nclass {}(RPCError):\n code = {}\n' .format(cls, int_code)) # Error classes generation for error in errors: f.write('\n\nclass {}({}):\n '.format(error.name, error.subclass)) if error.has_captures: f.write('def __init__(self, request, capture=0):\n ' ' self.request = request\n ') f.write(' self.{} = int(capture)\n ' .format(error.capture_name)) else: f.write('def __init__(self, request):\n ' ' self.request = request\n ') f.write('super(Exception, self).__init__(' '{}'.format(repr(error.description))) if error.has_captures: f.write('.format({0}=self.{0})'.format(error.capture_name)) f.write(' + self._fmt_request(self.request))\n\n') f.write(' def __reduce__(self):\n ') if error.has_captures: f.write('return type(self), (self.request, self.{})\n'.format(error.capture_name)) else: f.write('return type(self), (self.request,)\n') # Create the actual {CODE: ErrorClassName} dict once classes are defined f.write('\n\nrpc_errors_dict = {\n') for error in exact_match: f.write(' {}: {},\n'.format(repr(error.pattern), error.name)) f.write('}\n\nrpc_errors_re = (\n') for error in regex_match: f.write(' ({}, {}),\n'.format(repr(error.pattern), error.name)) f.write(')\n') Telethon-1.39.0/telethon_generator/generators/tlobject.py000066400000000000000000000641431475566265000236370ustar00rootroot00000000000000import functools import os import re import shutil import struct from collections import defaultdict from zlib import crc32 from ..sourcebuilder import SourceBuilder from ..utils import snake_to_camel_case AUTO_GEN_NOTICE = \ '"""File generated by TLObjects\' generator. All changes will be ERASED"""' AUTO_CASTS = { 'InputPeer': 'utils.get_input_peer(await client.get_input_entity({}))', 'InputChannel': 'utils.get_input_channel(await client.get_input_entity({}))', 'InputUser': 'utils.get_input_user(await client.get_input_entity({}))', 'InputDialogPeer': 'await client._get_input_dialog({})', 'InputNotifyPeer': 'await client._get_input_notify({})', 'InputMedia': 'utils.get_input_media({})', 'InputPhoto': 'utils.get_input_photo({})', 'InputMessage': 'utils.get_input_message({})', 'InputDocument': 'utils.get_input_document({})', 'InputChatPhoto': 'utils.get_input_chat_photo({})', 'InputGroupCall': 'utils.get_input_group_call({})', } NAMED_AUTO_CASTS = { ('chat_id', 'int'): 'await client.get_peer_id({}, add_mark=False)' } # Secret chats have a chat_id which may be negative. # With the named auto-cast above, we would break it. # However there are plenty of other legit requests # with `chat_id:int` where it is useful. # # NOTE: This works because the auto-cast is not recursive. # There are plenty of types that would break if we # did recurse into them to resolve them. NAMED_BLACKLIST = { 'messages.discardEncryption' } BASE_TYPES = ('string', 'bytes', 'int', 'long', 'int128', 'int256', 'double', 'Bool', 'true', 'date') def _write_modules( out_dir, depth, kind, namespace_tlobjects, type_constructors): # namespace_tlobjects: {'namespace', [TLObject]} out_dir.mkdir(parents=True, exist_ok=True) for ns, tlobjects in namespace_tlobjects.items(): file = out_dir / '{}.py'.format(ns or '__init__') with file.open('w') as f, SourceBuilder(f) as builder: builder.writeln(AUTO_GEN_NOTICE) builder.writeln('from {}.tl.tlobject import TLObject', '.' * depth) if kind != 'TLObject': builder.writeln( 'from {}.tl.tlobject import {}', '.' * depth, kind) builder.writeln('from typing import Optional, List, ' 'Union, TYPE_CHECKING') # Add the relative imports to the namespaces, # unless we already are in a namespace. if not ns: builder.writeln('from . import {}', ', '.join(sorted( x for x in namespace_tlobjects.keys() if x ))) # Import 'os' for those needing access to 'os.urandom()' # Currently only 'random_id' needs 'os' to be imported, # for all those TLObjects with arg.can_be_inferred. builder.writeln('import os') # Import struct for the .__bytes__(self) serialization builder.writeln('import struct') # Import datetime for type hinting builder.writeln('from datetime import datetime') tlobjects.sort(key=lambda x: x.name) type_names = set() type_defs = [] # Find all the types in this file and generate type definitions # based on the types. The type definitions are written to the # file at the end. for t in tlobjects: if not t.is_function: type_name = t.result if '.' in type_name: type_name = type_name[type_name.rindex('.'):] if type_name in type_names: continue type_names.add(type_name) constructors = type_constructors[type_name] if not constructors: pass elif len(constructors) == 1: type_defs.append('Type{} = {}'.format( type_name, constructors[0].class_name)) else: type_defs.append('Type{} = Union[{}]'.format( type_name, ','.join(c.class_name for c in constructors))) imports = {} primitives = {'int', 'long', 'int128', 'int256', 'double', 'string', 'date', 'bytes', 'Bool', 'true'} # Find all the types in other files that are used in this file # and generate the information required to import those types. for t in tlobjects: for arg in t.args: name = arg.type if not name or name in primitives: continue import_space = '{}.tl.types'.format('.' * depth) if '.' in name: namespace = name.split('.')[0] name = name.split('.')[1] import_space += '.{}'.format(namespace) if name not in type_names: type_names.add(name) if name == 'date': imports['datetime'] = ['datetime'] continue elif import_space not in imports: imports[import_space] = set() imports[import_space].add('Type{}'.format(name)) # Add imports required for type checking if imports: builder.writeln('if TYPE_CHECKING:') for namespace, names in imports.items(): builder.writeln('from {} import {}', namespace, ', '.join(sorted(names))) builder.end_block() # Generate the class for every TLObject for t in tlobjects: _write_source_code(t, kind, builder, type_constructors) builder.current_indent = 0 # Write the type definitions generated earlier. builder.writeln() for line in type_defs: builder.writeln(line) def _write_source_code(tlobject, kind, builder, type_constructors): """ Writes the source code corresponding to the given TLObject by making use of the ``builder`` `SourceBuilder`. Additional information such as file path depth and the ``Type: [Constructors]`` must be given for proper importing and documentation strings. """ _write_class_init(tlobject, kind, type_constructors, builder) _write_resolve(tlobject, builder) _write_to_dict(tlobject, builder) _write_to_bytes(tlobject, builder) _write_from_reader(tlobject, builder) _write_read_result(tlobject, builder) def _write_class_init(tlobject, kind, type_constructors, builder): builder.writeln() builder.writeln() builder.writeln('class {}({}):', tlobject.class_name, kind) # Class-level variable to store its Telegram's constructor ID builder.writeln('CONSTRUCTOR_ID = {:#x}', tlobject.id) builder.writeln('SUBCLASS_OF_ID = {:#x}', crc32(tlobject.result.encode('ascii'))) builder.writeln() # Convert the args to string parameters, those with flag having =None args = ['{}: {}{}'.format( a.name, a.type_hint(), '=None' if a.flag or a.can_be_inferred else '') for a in tlobject.real_args ] # Write the __init__ function if it has any argument if not tlobject.real_args: return if any(hasattr(__builtins__, a.name) for a in tlobject.real_args): builder.writeln('# noinspection PyShadowingBuiltins') builder.writeln("def __init__({}):", ', '.join(['self'] + args)) builder.writeln('"""') if tlobject.is_function: builder.write(':returns {}: ', tlobject.result) else: builder.write('Constructor for {}: ', tlobject.result) constructors = type_constructors[tlobject.result] if not constructors: builder.writeln('This type has no constructors.') elif len(constructors) == 1: builder.writeln('Instance of {}.', constructors[0].class_name) else: builder.writeln('Instance of either {}.', ', '.join( c.class_name for c in constructors)) builder.writeln('"""') # Set the arguments for arg in tlobject.real_args: if not arg.can_be_inferred: builder.writeln('self.{0} = {0}', arg.name) # Currently the only argument that can be # inferred are those called 'random_id' elif arg.name == 'random_id': # Endianness doesn't really matter, and 'big' is shorter code = "int.from_bytes(os.urandom({}), 'big', signed=True)" \ .format(8 if arg.type == 'long' else 4) if arg.is_vector: # Currently for the case of "messages.forwardMessages" # Ensure we can infer the length from id:Vector<> if not next(a for a in tlobject.real_args if a.name == 'id').is_vector: raise ValueError( 'Cannot infer list of random ids for ', tlobject ) code = '[{} for _ in range(len(id))]'.format(code) builder.writeln( "self.random_id = random_id if random_id " "is not None else {}", code ) else: raise ValueError('Cannot infer a value for ', arg) builder.end_block() def _write_resolve(tlobject, builder): if tlobject.is_function and any( (arg.type in AUTO_CASTS or ((arg.name, arg.type) in NAMED_AUTO_CASTS and tlobject.fullname not in NAMED_BLACKLIST)) for arg in tlobject.real_args ): builder.writeln('async def resolve(self, client, utils):') for arg in tlobject.real_args: ac = AUTO_CASTS.get(arg.type) if not ac: ac = NAMED_AUTO_CASTS.get((arg.name, arg.type)) if not ac: continue if arg.flag: builder.writeln('if self.{}:', arg.name) if arg.is_vector: builder.writeln('_tmp = []') builder.writeln('for _x in self.{0}:', arg.name) builder.writeln('_tmp.append({})', ac.format('_x')) builder.end_block() builder.writeln('self.{} = _tmp', arg.name) else: builder.writeln('self.{} = {}', arg.name, ac.format('self.' + arg.name)) if arg.flag: builder.end_block() builder.end_block() def _write_to_dict(tlobject, builder): builder.writeln('def to_dict(self):') builder.writeln('return {') builder.current_indent += 1 builder.write("'_': '{}'", tlobject.class_name) for arg in tlobject.real_args: builder.writeln(',') builder.write("'{}': ", arg.name) if arg.type in BASE_TYPES: if arg.is_vector: builder.write('[] if self.{0} is None else self.{0}[:]', arg.name) else: builder.write('self.{}', arg.name) else: if arg.is_vector: builder.write( '[] if self.{0} is None else [x.to_dict() ' 'if isinstance(x, TLObject) else x for x in self.{0}]', arg.name ) else: builder.write( 'self.{0}.to_dict() ' 'if isinstance(self.{0}, TLObject) else self.{0}', arg.name ) builder.writeln() builder.current_indent -= 1 builder.writeln("}") builder.end_block() def _write_to_bytes(tlobject, builder): builder.writeln('def _bytes(self):') # Some objects require more than one flag parameter to be set # at the same time. In this case, add an assertion. repeated_args = defaultdict(list) for arg in tlobject.args: if arg.flag: repeated_args[(arg.flag, arg.flag_index)].append(arg) for ra in repeated_args.values(): if len(ra) > 1: cnd1 = ('(self.{0} or self.{0} is not None)' .format(a.name) for a in ra) cnd2 = ('(self.{0} is None or self.{0} is False)' .format(a.name) for a in ra) builder.writeln( "assert ({}) or ({}), '{} parameters must all " "be False-y (like None) or all me True-y'", ' and '.join(cnd1), ' and '.join(cnd2), ', '.join(a.name for a in ra) ) builder.writeln("return b''.join((") builder.current_indent += 1 # First constructor code, we already know its bytes builder.writeln('{},', repr(struct.pack('/Vector. # If this weren't the case, we should check upper case after # max(index('<'), index('.')) (and if it is, it's boxed, so return). m = re.match(r'Vector<(int|long)>', tlobject.result) if not m: return builder.end_block() builder.writeln('@staticmethod') builder.writeln('def read_result(reader):') builder.writeln('reader.read_int() # Vector ID') builder.writeln('return [reader.read_{}() ' 'for _ in range(reader.read_int())]', m.group(1)) def _write_arg_to_bytes(builder, arg, tlobject, name=None): """ Writes the .__bytes__() code for the given argument :param builder: The source code builder :param arg: The argument to write :param tlobject: The parent TLObject :param name: The name of the argument. Defaults to "self.argname" This argument is an option because it's required when writing Vectors<> """ if arg.generic_definition: return # Do nothing, this only specifies a later type if name is None: name = 'self.{}'.format(arg.name) # The argument may be a flag, only write if it's not None AND # if it's not a True type. # True types are not actually sent, but instead only used to # determine the flags. if arg.flag: if arg.type == 'true': return # Exit, since True type is never written elif arg.is_vector: # 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 {0} is None or {0} is False " "else b''.join((", name) elif 'Bool' == arg.type: # `False` is a valid value for this type, so only check for `None`. builder.write("b'' if {0} is None else (", name) else: builder.write("b'' if {0} is None or {0} is False " "else (", name) if arg.is_vector: if arg.use_vector_id: # vector code, unsigned 0x1cb5c415 as little endian builder.write(r"b'\x15\xc4\xb5\x1c',") builder.write("struct.pack('3.5 feature, so add another join. builder.write("b''.join(") # Temporary disable .is_vector, not to enter this if again # Also disable .flag since it's not needed per element old_flag, arg.flag = arg.flag, None arg.is_vector = False _write_arg_to_bytes(builder, arg, tlobject, name='x') arg.is_vector = True arg.flag = old_flag builder.write(' for x in {})', name) elif arg.flag_indicator: # Calculate the flags with those items which are not None if not any(f.flag for f in tlobject.args): # There's a flag indicator, but no flag arguments so it's 0 builder.write(r"b'\0\0\0\0'") else: def fmt_flag_arg(a): if a.type == 'Bool': fmt = '(0 if {0} is None else {1})' else: fmt = '(0 if {0} is None or {0} is False else {1})' return fmt.format('self.{}'.format(a.name), 1 << a.flag_index) builder.write("struct.pack(' """ if arg.generic_definition: return # Do nothing, this only specifies a later type # The argument may be a flag, only write that flag was given! old_flag = None if arg.flag: # Treat 'true' flags as a special case, since they're true if # they're set, and nothing else needs to actually be read. if 'true' == arg.type: builder.writeln('{} = bool({} & {})', name, arg.flag, 1 << arg.flag_index) return builder.writeln('if {} & {}:', arg.flag, 1 << arg.flag_index) # Temporary disable .flag not to enter this if # again when calling the method recursively old_flag, arg.flag = arg.flag, None if arg.is_vector: if arg.use_vector_id: # We have to read the vector's constructor ID builder.writeln("reader.read_int()") builder.writeln('{} = []', name) builder.writeln('for _ in range(reader.read_int()):') # Temporary disable .is_vector, not to enter this if again arg.is_vector = False _write_arg_read_code(builder, arg, tlobject, name='_x') builder.writeln('{}.append(_x)', name) arg.is_vector = True elif arg.flag_indicator: # Read the flags, which will indicate what items we should read next builder.writeln('{} = reader.read_int()', arg.name) builder.writeln() elif 'int' == arg.type: # User IDs are becoming larger than 2³¹ - 1, which would translate # into reading a negative ID, which we would treat as a chat. So # special case them to read unsigned. See https://t.me/BotNews/57. if arg.name == 'user_id' or (arg.name == 'id' and tlobject.result == 'User'): builder.writeln('{} = reader.read_int(signed=False)', name) else: builder.writeln('{} = reader.read_int()', name) elif 'long' == arg.type: builder.writeln('{} = reader.read_long()', name) elif 'int128' == arg.type: builder.writeln('{} = reader.read_large_int(bits=128)', name) elif 'int256' == arg.type: builder.writeln('{} = reader.read_large_int(bits=256)', name) elif 'double' == arg.type: builder.writeln('{} = reader.read_double()', name) elif 'string' == arg.type: builder.writeln('{} = reader.tgread_string()', name) elif 'Bool' == arg.type: builder.writeln('{} = reader.tgread_bool()', name) elif 'true' == arg.type: # Arbitrary not-None value, don't actually read "true" flags builder.writeln('{} = True', name) elif 'bytes' == arg.type: builder.writeln('{} = reader.tgread_bytes()', name) elif 'date' == arg.type: # Custom format builder.writeln('{} = reader.tgread_date()', name) else: # Else it may be a custom type if not arg.skip_constructor_id: builder.writeln('{} = reader.tgread_object()', name) else: # Import the correct type inline to avoid cyclic imports. # There may be better solutions so that we can just access # all the types before the files have been parsed, but I # don't know of any. sep_index = arg.type.find('.') if sep_index == -1: ns, t = '.', arg.type else: ns, t = '.' + arg.type[:sep_index], arg.type[sep_index+1:] class_name = snake_to_camel_case(t) # There would be no need to import the type if we're in the # file with the same namespace, but since it does no harm # and we don't have information about such thing in the # method we just ignore that case. builder.writeln('from {} import {}', ns, class_name) builder.writeln('{} = {}.from_reader(reader)', name, class_name) # End vector and flag blocks if required (if we opened them before) if arg.is_vector: builder.end_block() if old_flag: builder.current_indent -= 1 builder.writeln('else:') builder.writeln('{} = None', name) builder.current_indent -= 1 # Restore .flag arg.flag = old_flag def _write_all_tlobjects(tlobjects, layer, builder): builder.writeln(AUTO_GEN_NOTICE) builder.writeln() builder.writeln('from . import types, functions') builder.writeln() # Create a constant variable to indicate which layer this is builder.writeln('LAYER = {}', layer) builder.writeln() # Then create the dictionary containing constructor_id: class builder.writeln('tlobjects = {') builder.current_indent += 1 # Fill the dictionary (0x1a2b3c4f: tl.full.type.path.Class) tlobjects.sort(key=lambda x: x.name) for tlobject in tlobjects: builder.write('{:#010x}: ', tlobject.id) builder.write('functions' if tlobject.is_function else 'types') if tlobject.namespace: builder.write('.{}', tlobject.namespace) builder.writeln('.{},', tlobject.class_name) builder.current_indent -= 1 builder.writeln('}') def generate_tlobjects(tlobjects, layer, import_depth, output_dir): # Group everything by {namespace: [tlobjects]} to generate __init__.py namespace_functions = defaultdict(list) namespace_types = defaultdict(list) # Group {type: [constructors]} to generate the documentation type_constructors = defaultdict(list) for tlobject in tlobjects: if tlobject.is_function: namespace_functions[tlobject.namespace].append(tlobject) else: namespace_types[tlobject.namespace].append(tlobject) type_constructors[tlobject.result].append(tlobject) _write_modules(output_dir / 'functions', import_depth, 'TLRequest', namespace_functions, type_constructors) _write_modules(output_dir / 'types', import_depth, 'TLObject', namespace_types, type_constructors) filename = output_dir / 'alltlobjects.py' with filename.open('w') as file: with SourceBuilder(file) as builder: _write_all_tlobjects(tlobjects, layer, builder) def clean_tlobjects(output_dir): for d in ('functions', 'types'): d = output_dir / d if d.is_dir(): shutil.rmtree(str(d)) tl = output_dir / 'alltlobjects.py' if tl.is_file(): tl.unlink() Telethon-1.39.0/telethon_generator/parsers/000077500000000000000000000000001475566265000207555ustar00rootroot00000000000000Telethon-1.39.0/telethon_generator/parsers/__init__.py000066400000000000000000000002271475566265000230670ustar00rootroot00000000000000from .errors import Error, parse_errors from .methods import MethodInfo, Usability, parse_methods from .tlobject import TLObject, parse_tl, find_layer Telethon-1.39.0/telethon_generator/parsers/errors.py000066400000000000000000000056141475566265000226510ustar00rootroot00000000000000import csv import re from ..utils import snake_to_camel_case # Core base classes depending on the integer error code KNOWN_BASE_CLASSES = { 303: 'InvalidDCError', 400: 'BadRequestError', 401: 'UnauthorizedError', 403: 'ForbiddenError', 404: 'NotFoundError', 406: 'AuthKeyError', 420: 'FloodError', 500: 'ServerError', 503: 'TimedOutError' } def _get_class_name(error_code): """ Gets the corresponding class name for the given error code, this either being an integer (thus base error name) or str. """ if isinstance(error_code, int): return KNOWN_BASE_CLASSES.get( abs(error_code), 'RPCError' + str(error_code).replace('-', 'Neg') ) if error_code.startswith('2'): error_code = re.sub(r'2', 'TWO_', error_code, count=1) if re.match(r'\d+', error_code): raise RuntimeError('error code starting with a digit cannot have valid Python name: {}'.format(error_code)) return snake_to_camel_case( error_code.replace('FIRSTNAME', 'FIRST_NAME')\ .replace('SLOWMODE', 'SLOW_MODE').lower(), suffix='Error') class Error: def __init__(self, codes, name, description): # TODO Some errors have the same name but different integer codes # Should these be split into different files or doesn't really matter? # Telegram isn't exactly consistent with returned errors anyway. self.int_code = codes[0] self.int_codes = codes self.str_code = name self.subclass = _get_class_name(codes[0]) self.subclass_exists = abs(codes[0]) in KNOWN_BASE_CLASSES self.description = description self.has_captures = '_X' in name if self.has_captures: self.name = _get_class_name(name.replace('_X', '_')) self.pattern = name.replace('_X', r'_(\d+)') self.capture_name = re.search(r'{(\w+)}', description).group(1) else: self.name = _get_class_name(name) self.pattern = name self.capture_name = None def parse_errors(csv_file): """ Parses the input CSV file with columns (name, error codes, description) and yields `Error` instances as a result. """ with csv_file.open(newline='') as f: f = csv.reader(f) next(f, None) # header for line, tup in enumerate(f, start=2): try: name, codes, description = tup except ValueError: raise ValueError('Columns count mismatch, unquoted comma in ' 'desc? (line {})'.format(line)) from None try: codes = [int(x) for x in codes.split()] or [400] except ValueError: raise ValueError('Not all codes are integers ' '(line {})'.format(line)) from None yield Error([int(x) for x in codes], name, description) Telethon-1.39.0/telethon_generator/parsers/methods.py000066400000000000000000000040641475566265000227760ustar00rootroot00000000000000import csv import enum import warnings class Usability(enum.Enum): UNKNOWN = 0 USER = 1 BOT = 2 BOTH = 4 @property def key(self): return { Usability.UNKNOWN: 'unknown', Usability.USER: 'user', Usability.BOT: 'bot', Usability.BOTH: 'both', }[self] class MethodInfo: def __init__(self, name, usability, errors, friendly): self.name = name self.errors = errors self.friendly = friendly try: self.usability = { 'unknown': Usability.UNKNOWN, 'user': Usability.USER, 'bot': Usability.BOT, 'both': Usability.BOTH, }[usability.lower()] except KeyError: raise ValueError('Usability must be either user, bot, both or ' 'unknown, not {}'.format(usability)) from None def parse_methods(csv_file, friendly_csv_file, errors_dict): """ Parses the input CSV file with columns (method, usability, errors) and yields `MethodInfo` instances as a result. """ raw_to_friendly = {} with friendly_csv_file.open(newline='') as f: f = csv.reader(f) next(f, None) # header for ns, friendly, raw_list in f: for raw in raw_list.split(): raw_to_friendly[raw] = (ns, friendly) with csv_file.open(newline='') as f: f = csv.reader(f) next(f, None) # header for line, (method, usability, errors) in enumerate(f, start=2): try: errors = [errors_dict[x] for x in errors.split()] except KeyError: raise ValueError('Method {} references unknown errors {}' .format(method, errors)) from None friendly = raw_to_friendly.pop(method, None) yield MethodInfo(method, usability, errors, friendly) if raw_to_friendly: warnings.warn('note: unknown raw methods in friendly mapping: {}' .format(', '.join(raw_to_friendly))) Telethon-1.39.0/telethon_generator/parsers/tlobject/000077500000000000000000000000001475566265000225635ustar00rootroot00000000000000Telethon-1.39.0/telethon_generator/parsers/tlobject/__init__.py000066400000000000000000000001411475566265000246700ustar00rootroot00000000000000from .tlarg import TLArg from .tlobject import TLObject from .parser import parse_tl, find_layer Telethon-1.39.0/telethon_generator/parsers/tlobject/parser.py000066400000000000000000000110051475566265000244260ustar00rootroot00000000000000import collections import re from .tlarg import TLArg from .tlobject import TLObject from ..methods import Usability CORE_TYPES = { 0xbc799737, # boolFalse#bc799737 = Bool; 0x997275b5, # boolTrue#997275b5 = Bool; 0x3fedd339, # true#3fedd339 = True; 0xc4b9f9bb, # error#c4b9f9bb code:int text:string = Error; 0x56730bcc # null#56730bcc = Null; } # Telegram Desktop (C++) doesn't care about string/bytes, and the .tl files # don't either. However in Python we *do*, and we want to deal with bytes # for the authorization key process, not UTF-8 strings (they won't be). # # Every type with an ID that's in here should get their attribute types # with string being replaced with bytes. AUTH_KEY_TYPES = { 0x05162463, # resPQ, 0x83c95aec, # p_q_inner_data 0xa9f55f95, # p_q_inner_data_dc 0x3c6a84d4, # p_q_inner_data_temp 0x56fddf88, # p_q_inner_data_temp_dc 0xd0e8075c, # server_DH_params_ok 0xb5890dba, # server_DH_inner_data 0x6643b654, # client_DH_inner_data 0xd712e4be, # req_DH_params 0xf5045f1f, # set_client_DH_params 0x3072cfa1 # gzip_packed } def _from_line(line, is_function, method_info, layer): match = re.match( r'^([\w.]+)' # 'name' r'(?:#([0-9a-fA-F]+))?' # '#optionalcode' r'(?:\s{?\w+:[\w\d<>#.?!]+}?)*' # '{args:.0?type}' r'\s=\s' # ' = ' r'([\w\d<>#.?]+);$', # ';' line ) if match is None: # Probably "vector#1cb5c415 {t:Type} # [ t ] = Vector t;" raise ValueError('Cannot parse TLObject {}'.format(line)) args_match = re.findall( r'({)?' r'(\w+)' r':' r'([\w\d<>#.?!]+)' r'}?', line ) name = match.group(1) method_info = method_info.get(name) if method_info: usability = method_info.usability friendly = method_info.friendly else: usability = Usability.UNKNOWN friendly = None return TLObject( fullname=name, object_id=match.group(2), result=match.group(3), is_function=is_function, layer=layer, usability=usability, friendly=friendly, args=[TLArg(name, arg_type, brace != '') for brace, name, arg_type in args_match] ) def parse_tl(file_path, layer, methods=None, ignored_ids=CORE_TYPES): """ This method yields TLObjects from a given .tl file. Note that the file is parsed completely before the function yields because references to other objects may appear later in the file. """ method_info = {m.name: m for m in (methods or [])} obj_all = [] obj_by_name = {} obj_by_type = collections.defaultdict(list) with file_path.open() as file: is_function = False for line in file: comment_index = line.find('//') if comment_index != -1: line = line[:comment_index] line = line.strip() if not line: continue match = re.match(r'---(\w+)---', line) if match: following_types = match.group(1) is_function = following_types == 'functions' continue try: result = _from_line( line, is_function, method_info, layer=layer) if result.id in ignored_ids: continue obj_all.append(result) if not result.is_function: obj_by_name[result.fullname] = result obj_by_type[result.result].append(result) except ValueError as e: if 'vector#1cb5c415' not in str(e): raise # Once all objects have been parsed, replace the # string type from the arguments with references for obj in obj_all: if obj.id in AUTH_KEY_TYPES: for arg in obj.args: if arg.type == 'string': arg.type = 'bytes' for arg in obj.args: arg.cls = obj_by_type.get(arg.type) or ( [obj_by_name[arg.type]] if arg.type in obj_by_name else [] ) yield from obj_all def find_layer(file_path): """Finds the layer used on the specified scheme.tl file.""" layer_regex = re.compile(r'^//\s*LAYER\s*(\d+)$') with file_path.open('r') as file: for line in file: match = layer_regex.match(line) if match: return int(match.group(1)) Telethon-1.39.0/telethon_generator/parsers/tlobject/tlarg.py000066400000000000000000000205271475566265000242540ustar00rootroot00000000000000import re def _fmt_strings(*dicts): for d in dicts: for k, v in d.items(): if v in ('None', 'True', 'False'): d[k] = '{}'.format(v) else: d[k] = re.sub( r'([brf]?([\'"]).*\2)', lambda m: '{}'.format(m.group(1)), v ) KNOWN_NAMED_EXAMPLES = { ('message', 'string'): "'Hello there!'", ('expires_at', 'date'): 'datetime.timedelta(minutes=5)', ('until_date', 'date'): 'datetime.timedelta(days=14)', ('view_messages', 'true'): 'None', ('send_messages', 'true'): 'None', ('limit', 'int'): '100', ('hash', 'int'): '0', ('hash', 'string'): "'A4LmkR23G0IGxBE71zZfo1'", ('min_id', 'int'): '0', ('max_id', 'int'): '0', ('min_id', 'long'): '0', ('max_id', 'long'): '0', ('add_offset', 'int'): '0', ('title', 'string'): "'My awesome title'", ('device_model', 'string'): "'ASUS Laptop'", ('system_version', 'string'): "'Arch Linux'", ('app_version', 'string'): "'1.0'", ('system_lang_code', 'string'): "'en'", ('lang_pack', 'string'): "''", ('lang_code', 'string'): "'en'", ('chat_id', 'int'): '478614198', ('client_id', 'long'): 'random.randrange(-2**63, 2**63)', ('video', 'InputFile'): "client.upload_file('/path/to/file.mp4')", } KNOWN_TYPED_EXAMPLES = { 'int128': "int.from_bytes(os.urandom(16), 'big')", 'bytes': "b'arbitrary\\x7f data \\xfa here'", 'long': "-12398745604826", 'string': "'some string here'", 'int': '42', 'date': 'datetime.datetime(2018, 6, 25)', 'double': '7.13', 'Bool': 'False', 'true': 'True', 'InputChatPhoto': "client.upload_file('/path/to/photo.jpg')", 'InputFile': "client.upload_file('/path/to/file.jpg')", 'InputPeer': "'username'" } _fmt_strings(KNOWN_NAMED_EXAMPLES, KNOWN_TYPED_EXAMPLES) SYNONYMS = { 'InputUser': 'InputPeer', 'InputChannel': 'InputPeer', 'InputDialogPeer': 'InputPeer', 'InputNotifyPeer': 'InputPeer', 'InputMessage': 'int' } # These are flags that are cleaner to leave off OMITTED_EXAMPLES = { 'silent', 'background', 'clear_draft', 'reply_to_msg_id', 'random_id', 'reply_markup', 'entities', 'embed_links', 'hash', 'min_id', 'max_id', 'add_offset', 'grouped', 'broadcast', 'admins', 'edit', 'delete' } class TLArg: def __init__(self, name, arg_type, generic_definition): """ Initializes a new .tl argument :param name: The name of the .tl argument :param arg_type: The type of the .tl argument :param generic_definition: Is the argument a generic definition? (i.e. {X:Type}) """ if name == 'self': self.name = 'is_self' elif name == 'from': self.name = 'from_' else: self.name = name # Default values self.is_vector = False self.flag = None # name of the flag to check if self is present self.skip_constructor_id = False self.flag_index = -1 # bit index of the flag to check if self is present self.cls = None # Special case: some types can be inferred, which makes it # less annoying to type. Currently the only type that can # be inferred is if the name is 'random_id', to which a # random ID will be assigned if left as None (the default) self.can_be_inferred = name == 'random_id' # The type can be an indicator that other arguments will be flags if arg_type == '#': self.flag_indicator = True self.type = None self.is_generic = False else: self.flag_indicator = False self.is_generic = arg_type.startswith('!') # Strip the exclamation mark always to have only the name self.type = arg_type.lstrip('!') # The type may be a flag (FLAGS.IDX?REAL_TYPE) # FLAGS can be any name, but it should have appeared previously. flag_match = re.match(r'(\w+).(\d+)\?([\w<>.]+)', self.type) if flag_match: self.flag = flag_match.group(1) self.flag_index = int(flag_match.group(2)) # Update the type to match the exact type, not the "flagged" one self.type = flag_match.group(3) # Then check if the type is a Vector vector_match = re.match(r'[Vv]ector<([\w\d.]+)>', self.type) if vector_match: self.is_vector = True # If the type's first letter is not uppercase, then # it is a constructor and we use (read/write) its ID # as pinpointed on issue #81. self.use_vector_id = self.type[0] == 'V' # Update the type to match the one inside the vector self.type = vector_match.group(1) # See use_vector_id. An example of such case is ipPort in # help.configSpecial if self.type.split('.')[-1][0].islower(): self.skip_constructor_id = True # The name may contain "date" in it, if this is the case and # the type is "int", we can safely assume that this should be # treated as a "date" object. Note that this is not a valid # Telegram object, but it's easier to work with if self.type == 'int' and ( re.search(r'(\b|_)(date|until|since)(\b|_)', name) or name in ('expires', 'expires_at', 'was_online')): self.type = 'date' self.generic_definition = generic_definition def type_hint(self): cls = self.type if '.' in cls: cls = cls.split('.')[1] result = { 'int': 'int', 'long': 'int', 'int128': 'int', 'int256': 'int', 'double': 'float', 'string': 'str', 'date': 'Optional[datetime]', # None date = 0 timestamp 'bytes': 'bytes', 'Bool': 'bool', 'true': 'bool', }.get(cls, "'Type{}'".format(cls)) if self.is_vector: result = 'List[{}]'.format(result) if self.flag and cls != 'date': result = 'Optional[{}]'.format(result) return result def real_type(self): # Find the real type representation by updating it as required real_type = self.type if self.flag_indicator: real_type = '#' if self.is_vector: if self.use_vector_id: real_type = 'Vector<{}>'.format(real_type) else: real_type = 'vector<{}>'.format(real_type) if self.is_generic: real_type = '!{}'.format(real_type) if self.flag: real_type = '{}.{}?{}'.format(self.flag, self.flag_index, real_type) return real_type def __str__(self): name = self.orig_name() if self.generic_definition: return '{{{}:{}}}'.format(name, self.real_type()) else: return '{}:{}'.format(name, self.real_type()) def __repr__(self): return str(self).replace(':date', ':int').replace('?date', '?int') def orig_name(self): return self.name.replace('is_self', 'self').strip('_') def to_dict(self): return { 'name': self.orig_name(), 'type': re.sub(r'\bdate$', 'int', self.real_type()) } def as_example(self, f, indent=0): if self.is_generic: f.write('other_request') return known = (KNOWN_NAMED_EXAMPLES.get((self.name, self.type)) or KNOWN_TYPED_EXAMPLES.get(self.type) or KNOWN_TYPED_EXAMPLES.get(SYNONYMS.get(self.type))) if known: f.write(known) return assert self.omit_example() or self.cls, 'TODO handle ' + str(self) # Pick an interesting example if any for cls in self.cls: if cls.is_good_example(): cls.as_example(f, indent) break else: # If no example is good, just pick the first self.cls[0].as_example(f, indent) def omit_example(self): return (self.flag or self.can_be_inferred) \ and self.name in OMITTED_EXAMPLES Telethon-1.39.0/telethon_generator/parsers/tlobject/tlobject.py000066400000000000000000000121321475566265000247420ustar00rootroot00000000000000import re import struct import zlib from ...utils import snake_to_camel_case # https://github.com/telegramdesktop/tdesktop/blob/4bf66cb6e93f3965b40084771b595e93d0b11bcd/Telegram/SourceFiles/codegen/scheme/codegen_scheme.py#L57-L62 WHITELISTED_MISMATCHING_IDS = { # 0 represents any layer 0: {'channel', # Since layer 77, there seems to be no going back... 'ipPortSecret', 'accessPointRule', 'help.configSimple'} } class TLObject: def __init__(self, fullname, object_id, args, result, is_function, usability, friendly, layer): """ Initializes a new TLObject, given its properties. :param fullname: The fullname of the TL object (namespace.name) The namespace can be omitted. :param object_id: The hexadecimal string representing the object ID :param args: The arguments, if any, of the TL object :param result: The result type of the TL object :param is_function: Is the object a function or a type? :param usability: The usability for this method. :param friendly: A tuple (namespace, friendly method name) if known. :param layer: The layer this TLObject belongs to. """ # The name can or not have a namespace self.fullname = fullname if '.' in fullname: self.namespace, self.name = fullname.split('.', maxsplit=1) else: self.namespace, self.name = None, fullname self.args = args self.result = result self.is_function = is_function self.usability = usability self.friendly = friendly self.id = None if object_id is None: self.id = self.infer_id() else: self.id = int(object_id, base=16) whitelist = WHITELISTED_MISMATCHING_IDS[0] |\ WHITELISTED_MISMATCHING_IDS.get(layer, set()) if self.fullname not in whitelist: assert self.id == self.infer_id(),\ 'Invalid inferred ID for ' + repr(self) self.class_name = snake_to_camel_case( self.name, suffix='Request' if self.is_function else '') self.real_args = list(a for a in self.sorted_args() if not (a.flag_indicator or a.generic_definition)) @property def innermost_result(self): index = self.result.find('<') if index == -1: return self.result else: return self.result[index + 1:-1] def sorted_args(self): """Returns the arguments properly sorted and ready to plug-in into a Python's method header (i.e., flags and those which can be inferred will go last so they can default =None) """ return sorted(self.args, key=lambda x: bool(x.flag) or x.can_be_inferred) def __repr__(self, ignore_id=False): if self.id is None or ignore_id: hex_id = '' else: hex_id = '#{:08x}'.format(self.id) if self.args: args = ' ' + ' '.join([repr(arg) for arg in self.args]) else: args = '' return '{}{}{} = {}'.format(self.fullname, hex_id, args, self.result) def infer_id(self): representation = self.__repr__(ignore_id=True) representation = representation\ .replace(':bytes ', ':string ')\ .replace('?bytes ', '?string ')\ .replace('<', ' ').replace('>', '')\ .replace('{', '').replace('}', '') # Remove optional empty values (special-cased to the true type) representation = re.sub( r' \w+:\w+\.\d+\?true', r'', representation ) return zlib.crc32(representation.encode('ascii')) def to_dict(self): return { 'id': str(struct.unpack('i', struct.pack('I', self.id))[0]), 'method' if self.is_function else 'predicate': self.fullname, 'params': [x.to_dict() for x in self.args if not x.generic_definition], 'type': self.result } def is_good_example(self): return not self.class_name.endswith('Empty') def as_example(self, f, indent=0): f.write('functions' if self.is_function else 'types') if self.namespace: f.write('.') f.write(self.namespace) f.write('.') f.write(self.class_name) f.write('(') args = [arg for arg in self.real_args if not arg.omit_example()] if not args: f.write(')') return f.write('\n') indent += 1 remaining = len(args) for arg in args: remaining -= 1 f.write(' ' * indent) f.write(arg.name) f.write('=') if arg.is_vector: f.write('[') arg.as_example(f, indent) if arg.is_vector: f.write(']') if remaining: f.write(',') f.write('\n') indent -= 1 f.write(' ' * indent) f.write(')') Telethon-1.39.0/telethon_generator/sourcebuilder.py000066400000000000000000000041621475566265000225220ustar00rootroot00000000000000class SourceBuilder: """This class should be used to build .py source files""" def __init__(self, out_stream, indent_size=4): self.current_indent = 0 self.on_new_line = False self.indent_size = indent_size self.out_stream = out_stream # Was a new line added automatically before? If so, avoid it self.auto_added_line = False def indent(self): """Indents the current source code line by the current indentation level """ self.write(' ' * (self.current_indent * self.indent_size)) def write(self, string, *args, **kwargs): """Writes a string into the source code, applying indentation if required """ if self.on_new_line: self.on_new_line = False # We're not on a new line anymore # If the string was not empty, indent; Else probably a new line if string.strip(): self.indent() if args or kwargs: self.out_stream.write(string.format(*args, **kwargs)) else: self.out_stream.write(string) def writeln(self, string='', *args, **kwargs): """Writes a string into the source code _and_ appends a new line, applying indentation if required """ self.write(string + '\n', *args, **kwargs) self.on_new_line = True # If we're writing a block, increment indent for the next time if string and string[-1] == ':': self.current_indent += 1 # Clear state after the user adds a new line self.auto_added_line = False def end_block(self): """Ends an indentation block, leaving an empty line afterwards""" self.current_indent -= 1 # If we did not add a new line automatically yet, now it's the time! if not self.auto_added_line: self.writeln() self.auto_added_line = True def __str__(self): self.out_stream.seek(0) return self.out_stream.read() def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.out_stream.close() Telethon-1.39.0/telethon_generator/syncerrors.py000066400000000000000000000044051475566265000220640ustar00rootroot00000000000000# Should be fed with the JSON obtained from https://core.telegram.org/api/errors#error-database import re import csv import sys import json from pathlib import Path sys.path.insert(0, '..') from telethon_generator.parsers.errors import parse_errors, Error from telethon_generator.parsers.methods import parse_methods, MethodInfo ERRORS = Path('data/errors.csv') METHODS = Path('data/methods.csv') FRIENDLY = Path('data/friendly.csv') def main(): new_errors = [] new_methods = [] self_errors = {e.str_code: e for e in parse_errors(ERRORS)} self_methods = {m.name: m for m in parse_methods(METHODS, FRIENDLY, self_errors)} tg_data = json.load(sys.stdin) def get_desc(code): return re.sub(r'\s*&\w+;\s*', '', (tg_data['descriptions'].get(code) or '').rstrip('.')) for int_code, errors in tg_data['errors'].items(): int_code = int(int_code) # json does not support non-string keys for code, methods in errors.items(): if not re.match(r'\w+', code): continue # skip, full code is unknown (contains asterisk or is multiple words) str_code = code.replace('%d', 'X') if error := self_errors.get(str_code): error.int_codes.append(int_code) # de-duplicated once later if not error.description: # prefer our descriptions if not error.has_captures: # need descriptions with specific text if error has captures error.description = get_desc(code) else: self_errors[str_code] = Error([int_code], str_code, get_desc(code)) new_errors.extend((e.str_code, ' '.join(map(str, sorted(set(e.int_codes)))), e.description) for e in self_errors.values()) new_methods.extend((m.name, m.usability.key, ' '.join(sorted(e.str_code for e in m.errors))) for m in self_methods.values()) csv.register_dialect('plain', lineterminator='\n') with ERRORS.open('w', encoding='utf-8', newline='') as fd: csv.writer(fd, 'plain').writerows((('name', 'codes', 'description'), *sorted(new_errors))) with METHODS.open('w', encoding='utf-8', newline='') as fd: csv.writer(fd, 'plain').writerows((('method', 'usability', 'errors'), *sorted(new_methods))) if __name__ == '__main__': main() Telethon-1.39.0/telethon_generator/utils.py000066400000000000000000000004521475566265000210110ustar00rootroot00000000000000import re def snake_to_camel_case(name, suffix=None): # Courtesy of http://stackoverflow.com/a/31531797/4759433 result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(), name) result = result[:1].upper() + result[1:].replace('_', '') return result + suffix if suffix else result Telethon-1.39.0/tests/000077500000000000000000000000001475566265000145505ustar00rootroot00000000000000Telethon-1.39.0/tests/__init__.py000066400000000000000000000000001475566265000166470ustar00rootroot00000000000000Telethon-1.39.0/tests/readthedocs/000077500000000000000000000000001475566265000170355ustar00rootroot00000000000000Telethon-1.39.0/tests/readthedocs/__init__.py000066400000000000000000000000001475566265000211340ustar00rootroot00000000000000Telethon-1.39.0/tests/readthedocs/conftest.py000066400000000000000000000001471475566265000212360ustar00rootroot00000000000000import pathlib import pytest @pytest.fixture def docs_dir(): return pathlib.Path('readthedocs') Telethon-1.39.0/tests/readthedocs/quick_references/000077500000000000000000000000001475566265000223525ustar00rootroot00000000000000Telethon-1.39.0/tests/readthedocs/quick_references/__init__.py000066400000000000000000000000001475566265000244510ustar00rootroot00000000000000Telethon-1.39.0/tests/readthedocs/quick_references/test_client_reference.py000066400000000000000000000010071475566265000272550ustar00rootroot00000000000000import re from telethon import TelegramClient def test_all_methods_present(docs_dir): with (docs_dir / 'quick-references/client-reference.rst').open(encoding='utf-8') as fd: present_methods = set(map(str.lstrip, re.findall(r'^ {4}\w+$', fd.read(), re.MULTILINE))) assert len(present_methods) > 0 for name in dir(TelegramClient): attr = getattr(TelegramClient, name) if callable(attr) and not name.startswith('_') and name != 'sign_up': assert name in present_methods Telethon-1.39.0/tests/telethon/000077500000000000000000000000001475566265000163725ustar00rootroot00000000000000Telethon-1.39.0/tests/telethon/__init__.py000066400000000000000000000000001475566265000204710ustar00rootroot00000000000000Telethon-1.39.0/tests/telethon/client/000077500000000000000000000000001475566265000176505ustar00rootroot00000000000000Telethon-1.39.0/tests/telethon/client/__init__.py000066400000000000000000000000001475566265000217470ustar00rootroot00000000000000Telethon-1.39.0/tests/telethon/client/test_messages.py000066400000000000000000000055471475566265000231030ustar00rootroot00000000000000import inspect from unittest import mock from unittest.mock import MagicMock import pytest from telethon import TelegramClient from telethon.client import MessageMethods from telethon.tl.types import PeerChat, MessageMediaDocument, Message, MessageEntityBold @pytest.mark.asyncio async def test_send_message_with_file_forwards_args(): arguments = {} sentinel = object() for value, name in enumerate(inspect.signature(TelegramClient.send_message).parameters): if name in {'self', 'entity', 'file'}: continue # positional if name in {'message'}: continue # renamed if name in {'link_preview'}: continue # make no sense in send_file arguments[name] = value class MockedClient(TelegramClient): # noinspection PyMissingConstructor def __init__(self): pass async def send_file(self, entity, file, **kwargs): assert entity == 'a' assert file == 'b' for k, v in arguments.items(): assert k in kwargs assert kwargs[k] == v return sentinel client = MockedClient() assert (await client.send_message('a', file='b', **arguments)) == sentinel class TestMessageMethods: @pytest.mark.asyncio @pytest.mark.parametrize( 'formatting_entities', ([MessageEntityBold(offset=0, length=0)], None) ) async def test_send_msg_and_file(self, formatting_entities): async def async_func(result): # AsyncMock was added only in 3.8 return result msg_methods = MessageMethods() expected_result = Message( id=0, peer_id=PeerChat(chat_id=0), message='', date=None, ) entity = 'test_entity' message = Message( id=1, peer_id=PeerChat(chat_id=0), message='expected_caption', date=None, entities=[MessageEntityBold(offset=9, length=9)], ) media_file = MessageMediaDocument() with mock.patch.object( target=MessageMethods, attribute='send_file', new=MagicMock(return_value=async_func(expected_result)), create=True, ) as mock_obj: result = await msg_methods.send_message( entity=entity, message=message, file=media_file, formatting_entities=formatting_entities, ) mock_obj.assert_called_once_with( entity, media_file, caption=message.message, formatting_entities=formatting_entities or message.entities, reply_to=None, silent=None, attributes=None, parse_mode=(), force_document=False, thumb=None, buttons=None, clear_draft=False, schedule=None, supports_streaming=False, comment_to=None, background=None, nosound_video=None, ) assert result == expected_result Telethon-1.39.0/tests/telethon/crypto/000077500000000000000000000000001475566265000177125ustar00rootroot00000000000000Telethon-1.39.0/tests/telethon/crypto/__init__.py000066400000000000000000000000001475566265000220110ustar00rootroot00000000000000Telethon-1.39.0/tests/telethon/crypto/test_rsa.py000066400000000000000000000022171475566265000221120ustar00rootroot00000000000000""" Tests for `telethon.crypto.rsa`. """ import pytest from telethon.crypto import rsa @pytest.fixture def server_key_fp(): """Factory to return a key, old if so chosen.""" def _server_key_fp(old: bool): for fp, data in rsa._server_keys.items(): _, old_key = data if old_key == old: return fp return _server_key_fp def test_encryption_inv_key(): """Test for #1324.""" assert rsa.encrypt("invalid", b"testdata") is None def test_encryption_old_key(server_key_fp): """Test for #1324.""" assert rsa.encrypt(server_key_fp(old=True), b"testdata") is None def test_encryption_allowed_old_key(server_key_fp): data = rsa.encrypt(server_key_fp(old=True), b"testdata", use_old=True) # We can't verify the data is actually valid because we don't have # the decryption keys assert data is not None and len(data) == 256 def test_encryption_current_key(server_key_fp): data = rsa.encrypt(server_key_fp(old=False), b"testdata") # We can't verify the data is actually valid because we don't have # the decryption keys assert data is not None and len(data) == 256 Telethon-1.39.0/tests/telethon/events/000077500000000000000000000000001475566265000176765ustar00rootroot00000000000000Telethon-1.39.0/tests/telethon/events/__init__.py000066400000000000000000000000001475566265000217750ustar00rootroot00000000000000Telethon-1.39.0/tests/telethon/events/test_chataction.py000066400000000000000000000030751475566265000234310ustar00rootroot00000000000000import pytest from telethon import TelegramClient, events, types, utils def get_client(): return TelegramClient(None, 1, '1') def get_user_456(): return types.User( id=456, access_hash=789, first_name='User 123' ) @pytest.mark.asyncio async def test_get_input_users_no_action_message_no_entities(): event = events.ChatAction.build(types.UpdateChatParticipantDelete( chat_id=123, user_id=456, version=1 )) event._set_client(get_client()) assert await event.get_input_users() == [] @pytest.mark.asyncio async def test_get_input_users_no_action_message(): user = get_user_456() event = events.ChatAction.build(types.UpdateChatParticipantDelete( chat_id=123, user_id=456, version=1 )) event._set_client(get_client()) event._entities[user.id] = user assert await event.get_input_users() == [utils.get_input_peer(user)] @pytest.mark.asyncio async def test_get_users_no_action_message_no_entities(): event = events.ChatAction.build(types.UpdateChatParticipantDelete( chat_id=123, user_id=456, version=1 )) event._set_client(get_client()) assert await event.get_users() == [] @pytest.mark.asyncio async def test_get_users_no_action_message(): user = get_user_456() event = events.ChatAction.build(types.UpdateChatParticipantDelete( chat_id=123, user_id=456, version=1 )) event._set_client(get_client()) event._entities[user.id] = user assert await event.get_users() == [user] Telethon-1.39.0/tests/telethon/extensions/000077500000000000000000000000001475566265000205715ustar00rootroot00000000000000Telethon-1.39.0/tests/telethon/extensions/__init__.py000066400000000000000000000000001475566265000226700ustar00rootroot00000000000000Telethon-1.39.0/tests/telethon/extensions/test_html.py000066400000000000000000000054221475566265000231510ustar00rootroot00000000000000""" Tests for `telethon.extensions.html`. """ from telethon.extensions import html from telethon.tl.types import MessageEntityBold, MessageEntityItalic, MessageEntityTextUrl def test_entity_edges(): """ Test that entities at the edges (start and end) don't crash. """ text = 'Hello, world' entities = [MessageEntityBold(0, 5), MessageEntityBold(7, 5)] result = html.unparse(text, entities) assert result == 'Hello, world' def test_malformed_entities(): """ Test that malformed entity offsets from bad clients don't crash and produce the expected results. """ text = '🏆Telegram Official Android Challenge is over🏆.' entities = [MessageEntityTextUrl(offset=2, length=43, url='https://example.com')] result = html.unparse(text, entities) assert result == '🏆Telegram Official Android Challenge is over🏆.' def test_trailing_malformed_entities(): """ Similar to `test_malformed_entities`, but for the edge case where the malformed entity offset is right at the end (note the lack of a trailing dot in the text string). """ text = '🏆Telegram Official Android Challenge is over🏆' entities = [MessageEntityTextUrl(offset=2, length=43, url='https://example.com')] result = html.unparse(text, entities) assert result == '🏆Telegram Official Android Challenge is over🏆' def test_entities_together(): """ Test that an entity followed immediately by a different one behaves well. """ original = '⚙️Settings' stripped = '⚙️Settings' text, entities = html.parse(original) assert text == stripped assert entities == [MessageEntityBold(0, 2), MessageEntityItalic(2, 8)] text = html.unparse(text, entities) assert text == original def test_nested_entities(): """ Test that an entity nested inside another one behaves well. """ original = 'Example' original_entities = [MessageEntityTextUrl(0, 7, url='https://example.com'), MessageEntityBold(0, 7)] stripped = 'Example' text, entities = html.parse(original) assert text == stripped assert entities == original_entities text = html.unparse(text, entities) assert text == original def test_offset_at_emoji(): """ Tests that an entity starting at a emoji preserves the emoji. """ text = 'Hi\n👉 See example' entities = [MessageEntityBold(0, 2), MessageEntityItalic(3, 2), MessageEntityBold(10, 7)] parsed = 'Hi\n👉 See example' assert html.parse(parsed) == (text, entities) assert html.unparse(text, entities) == parsed Telethon-1.39.0/tests/telethon/extensions/test_markdown.py000066400000000000000000000052331475566265000240270ustar00rootroot00000000000000""" Tests for `telethon.extensions.markdown`. """ from telethon.extensions import markdown from telethon.tl.types import MessageEntityBold, MessageEntityItalic, MessageEntityTextUrl def test_entity_edges(): """ Test that entities at the edges (start and end) don't crash. """ text = 'Hello, world' entities = [MessageEntityBold(0, 5), MessageEntityBold(7, 5)] result = markdown.unparse(text, entities) assert result == '**Hello**, **world**' def test_malformed_entities(): """ Test that malformed entity offsets from bad clients don't crash and produce the expected results. """ text = '🏆Telegram Official Android Challenge is over🏆.' entities = [MessageEntityTextUrl(offset=2, length=43, url='https://example.com')] result = markdown.unparse(text, entities) assert result == "🏆[Telegram Official Android Challenge is over](https://example.com)🏆." def test_trailing_malformed_entities(): """ Similar to `test_malformed_entities`, but for the edge case where the malformed entity offset is right at the end (note the lack of a trailing dot in the text string). """ text = '🏆Telegram Official Android Challenge is over🏆' entities = [MessageEntityTextUrl(offset=2, length=43, url='https://example.com')] result = markdown.unparse(text, entities) assert result == "🏆[Telegram Official Android Challenge is over](https://example.com)🏆" def test_entities_together(): """ Test that an entity followed immediately by a different one behaves well. """ original = '**⚙️**__Settings__' stripped = '⚙️Settings' text, entities = markdown.parse(original) assert text == stripped assert entities == [MessageEntityBold(0, 2), MessageEntityItalic(2, 8)] text = markdown.unparse(text, entities) assert text == original def test_nested_entities(): """ Test that an entity nested inside another one behaves well. """ original = '**[Example](https://example.com)**' stripped = 'Example' text, entities = markdown.parse(original) assert text == stripped assert entities == [MessageEntityBold(0, 7), MessageEntityTextUrl(0, 7, url='https://example.com')] text = markdown.unparse(text, entities) assert text == original def test_offset_at_emoji(): """ Tests that an entity starting at a emoji preserves the emoji. """ text = 'Hi\n👉 See example' entities = [MessageEntityBold(0, 2), MessageEntityItalic(3, 2), MessageEntityBold(10, 7)] parsed = '**Hi**\n__👉__ See **example**' assert markdown.parse(parsed) == (text, entities) assert markdown.unparse(text, entities) == parsed Telethon-1.39.0/tests/telethon/test_helpers.py000066400000000000000000000051631475566265000214520ustar00rootroot00000000000000""" tests for telethon.helpers """ from base64 import b64decode import pytest from telethon import helpers from telethon.utils import get_inner_text from telethon.tl.types import MessageEntityUnknown as Meu def test_strip_text(): text = ' text ' text_stripped = 'text' entities_before_and_after = ( ([], []), ([Meu(i, 0) for i in range(10)], []), # del '' ([Meu(0, 0), Meu(0, 1), Meu(5, 1)], []), # del '', ' ', ' ' ([Meu(0, 3)], [Meu(0, 2)]), # ' te' -> 'te' ([Meu(3, 1)], [Meu(2, 1)]), # 'x' ([Meu(3, 2)], [Meu(2, 2)]), # 'xt' ([Meu(3, 3)], [Meu(2, 2)]), # 'xt ' -> 'xt' ([Meu(0, 6)], [Meu(0, 4)]), # ' text ' -> 'text' ) for entities_before, entities_expected in entities_before_and_after: entities_for_test = [Meu(meu.offset, meu.length) for meu in entities_before] # deep copy text_after = helpers.strip_text(text, entities_for_test) assert text_after == text_stripped assert sorted((e.offset, e.length) for e in entities_for_test) \ == sorted((e.offset, e.length) for e in entities_expected) inner_text_before = get_inner_text(text, entities_before) inner_text_before_stripped = [t.strip() for t in inner_text_before] inner_text_after = get_inner_text(text_after, entities_for_test) for t in inner_text_after: assert t in inner_text_before_stripped class TestSyncifyAsyncContext: class NoopContextManager: def __init__(self, loop): self.count = 0 self.loop = loop async def __aenter__(self): self.count += 1 return self async def __aexit__(self, exc_type, *args): assert exc_type is None self.count -= 1 __enter__ = helpers._sync_enter __exit__ = helpers._sync_exit def test_sync_acontext(self, event_loop): contm = self.NoopContextManager(event_loop) assert contm.count == 0 with contm: assert contm.count == 1 assert contm.count == 0 @pytest.mark.asyncio async def test_async_acontext(self, event_loop): contm = self.NoopContextManager(event_loop) assert contm.count == 0 async with contm: assert contm.count == 1 assert contm.count == 0 def test_generate_key_data_from_nonce(): gkdfn = helpers.generate_key_data_from_nonce key_expect = b64decode(b'NFwRFB8Knw/kAmvPWjtrQauWysHClVfQh0UOAaABqZA=') nonce_expect = b64decode(b'1AgjhU9eDvJRjFik73bjR2zZEATzL/jLu9yodYfWEgA=') assert gkdfn(123456789, 1234567) == (key_expect, nonce_expect) Telethon-1.39.0/tests/telethon/test_pickle.py000066400000000000000000000022101475566265000212450ustar00rootroot00000000000000import pickle from telethon.errors import RPCError, BadRequestError, FileIdInvalidError, NetworkMigrateError def _assert_equality(error, unpickled_error): assert error.code == unpickled_error.code assert error.message == unpickled_error.message assert type(error) == type(unpickled_error) assert str(error) == str(unpickled_error) def test_base_rpcerror_pickle(): error = RPCError("request", "message", 123) unpickled_error = pickle.loads(pickle.dumps(error)) _assert_equality(error, unpickled_error) def test_rpcerror_pickle(): error = BadRequestError("request", "BAD_REQUEST", 400) unpickled_error = pickle.loads(pickle.dumps(error)) _assert_equality(error, unpickled_error) def test_fancy_rpcerror_pickle(): error = FileIdInvalidError("request") unpickled_error = pickle.loads(pickle.dumps(error)) _assert_equality(error, unpickled_error) def test_fancy_rpcerror_capture_pickle(): error = NetworkMigrateError(request="request", capture=5) unpickled_error = pickle.loads(pickle.dumps(error)) _assert_equality(error, unpickled_error) assert error.new_dc == unpickled_error.new_dc Telethon-1.39.0/tests/telethon/test_utils.py000066400000000000000000000033701475566265000211460ustar00rootroot00000000000000import io import pathlib import pytest from telethon import utils from telethon.tl.types import ( MessageMediaGame, Game, PhotoEmpty ) def test_game_input_media_memory_error(): large_long = 2**62 media = MessageMediaGame(Game( id=large_long, # <- key to trigger `MemoryError` access_hash=large_long, short_name='short_name', title='title', description='description', photo=PhotoEmpty(large_long), )) input_media = utils.get_input_media(media) bytes(input_media) # <- shouldn't raise `MemoryError` def test_private_get_extension(): # Positive cases png_header = bytes.fromhex('89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52') png_buffer = io.BytesIO(png_header) class CustomFd: def __init__(self, name): self.name = name assert utils._get_extension('foo.bar.baz') == '.baz' assert utils._get_extension(pathlib.Path('foo.bar.baz')) == '.baz' assert utils._get_extension(CustomFd('foo.bar.baz')) == '.baz' # Negative cases null_header = bytes.fromhex('00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00') null_buffer = io.BytesIO(null_header) empty_header = bytes() empty_buffer = io.BytesIO(empty_header) assert utils._get_extension('foo') == '' assert utils._get_extension(pathlib.Path('foo')) == '' assert utils._get_extension(null_header) == '' assert utils._get_extension(null_buffer) == '' assert utils._get_extension(null_buffer) == '' # make sure it did seek back assert utils._get_extension(empty_header) == '' assert utils._get_extension(empty_buffer) == '' assert utils._get_extension(empty_buffer) == '' # make sure it did seek back assert utils._get_extension(CustomFd('foo')) == '' Telethon-1.39.0/tests/telethon/tl/000077500000000000000000000000001475566265000170115ustar00rootroot00000000000000Telethon-1.39.0/tests/telethon/tl/__init__.py000066400000000000000000000000001475566265000211100ustar00rootroot00000000000000Telethon-1.39.0/tests/telethon/tl/test_serialization.py000066400000000000000000000005451475566265000233030ustar00rootroot00000000000000import pytest from telethon.tl import types, functions def test_nested_invalid_serialization(): large_long = 2**62 request = functions.account.SetPrivacyRequest( key=types.InputPrivacyKeyChatInvite(), rules=[types.InputPrivacyValueDisallowUsers(users=[large_long])] ) with pytest.raises(TypeError): bytes(request) Telethon-1.39.0/update-docs.sh000066400000000000000000000005401475566265000161510ustar00rootroot00000000000000#!/bin/bash set -e python setup.py gen docs rm -rf /tmp/docs mv docs/ /tmp/docs git checkout gh-pages # there's probably better ways but we know none has spaces rm -rf $(ls /tmp/docs) mv /tmp/docs/* . git add constructors/ types/ methods/ index.html js/search.js css/ img/ git commit --amend -m "Update documentation" git push --force git checkout v1